diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000000..ffc0150ebac5 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/devcontainers/typescript-node:22-bookworm + +RUN apt-get install -y wget bzip2 + +# 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 + +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 + + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000000..67a8833d30cf --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,30 @@ +// For format details, see https://aka.ms/devcontainer.json. +{ + "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", + + "containerEnv": { + "CI_PYTHON_PATH": "/workspaces/vscode-python/.venv/bin/python" + } + +} 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 7c2542a3c660..000000000000 --- a/.eslintrc +++ /dev/null @@ -1,15 +0,0 @@ - -{ - "env": { - "node": true, - "es6": true - }, - "rules": { - "no-console": 0, - "no-cond-assign": 0, - "no-unused-vars": 1, - "no-extra-semi": "warn", - "semi": "warn" - }, - "extends": "eslint:recommended" -} 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/.gitattributes b/.gitattributes index f36040d43639..e25c2877c07f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ package.json text eol=lf package-lock.json text eol=lf +requirements.txt text eol=lf diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 6d04d0a7aafa..000000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,46 +0,0 @@ - - -## 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 - -## Expected behaviour - -XXX - -## Actual behaviour - -XXX - -## Steps to reproduce: -1. XXX - -## Logs -Output for `Python` in the `Output` panel (`View`→`Output`, change the drop-down the upper-right of the `Output` panel to `Python`) - -``` -XXX -``` - -Output from `Console` under the `Developer Tools` panel (toggle Developer Tools on under `Help`; turn on source maps to make any tracebacks be useful by running `Enable source map support for extension debugging`) - -``` -XXX -``` 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/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..c966f6bde856 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,17 @@ +blank_issues_enabled: false +contact_links: + - 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 3b32f16ead56..000000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,12 +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!) -- [ ] 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) 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/codecov.yml b/.github/codecov.yml deleted file mode 100644 index f16d332d04b0..000000000000 --- a/.github/codecov.yml +++ /dev/null @@ -1,4 +0,0 @@ -coverage: - precision: 0 - round: up - range: "70...90" diff --git a/.github/commands.json b/.github/commands.json new file mode 100644 index 000000000000..171f33f380c3 --- /dev/null +++ b/.github/commands.json @@ -0,0 +1,166 @@ +[ + { + "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!" + }, + { + "type": "label", + "name": "~spam", + "removeLabel": "~spam", + "addLabel": "spam", + "action": "close", + "reason": "not_planned", + "comment": "Thank you for your submission. This issue has been closed as it doesn't meet our community guidelines or appears to be spam.\n\n**If you believe this was closed in error:**\n- Please review our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/)\n- Ensure your issue contains a clear description of the problem or feature request\n- Feel free to open a new issue with appropriate detail if this was a legitimate concern\n\n**For legitimate issues, please include:**\n- Clear description of the problem\n- Steps to reproduce (for bugs)\n- Expected vs actual behavior\n- VS Code version and environment details\n\nThank you for helping us maintain a welcoming and productive community." + } +] 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 dde77291a4ef..000000000000 --- a/.github/lock.yml +++ /dev/null @@ -1,3 +0,0 @@ -daysUntilLock: 28 -lockComment: false -only: issues 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 8cb04d81415b..091ed559825b 100644 --- a/.github/release_plan.md +++ b/.github/release_plan.md @@ -1,83 +1,138 @@ -# Beta (Tuesday, XXX XX) - -- [ ] 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 -- [ ] Update [`CHANGELOG.md`](https://github.com/Microsoft/vscode-python/blob/master/CHANGELOG.md) - - [ ] Create a new section for this release - - [ ] Run [`news`](https://github.com/Microsoft/vscode-python/tree/master/news) (typically `python news | code-insiders -`) - - [ ] Touch up news entries (and corresponding news entry files) - - [ ] Copy over the "Thanks" section from the previous release -- [ ] Update [`ThirdPartyNotices-Distribution.txt`](https://github.com/Microsoft/vscode-python/blob/master/ThirdPartyNotices-Distribution.txt) - - [ ] Run [`tpn`](https://github.com/Microsoft/vscode-python/tree/master/tpn) (typically `python tpn --npm package-lock.json --npm-overrides package.datascience-ui.dependencies.json --config tpn/distribution.toml ThirdPartyNotices-Distribution.txt`) - - [ ] Register any Python changes with [OSPO](https://opensource.microsoft.com/) -- [ ] Update [`ThirdPartyNotices-Repository.txt`](https://github.com/Microsoft/vscode-python/blob/master/ThirdPartyNotices-Repository.txt) and register any changes with OSPO -- [ ] Open appropriate [documentation issues](https://github.com/microsoft/vscode-docs/issues?q=is%3Aissue+is%3Aopen+label%3Apython) -- [ ] Check that component governance is happy (requires beta PR to have been merged) - - -# Release candidate (Tuesday, XXX XX) - -- [ ] Ensure all new features are tracked via telemetry -- [ ] Announce a code freeze -- [ ] Create a branch against `master` 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 -- [ ] 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) (typically `python news --final | code-insiders -`; the `--final` flag is on purpose as no more changes are expected) - - [ ] Touch up news entries (and corresponding news entry files) - - [ ] Check that the "Thanks" section is up-to-date -- [ ] Update [`ThirdPartyNotices-Distribution.txt`](https://github.com/Microsoft/vscode-python/blob/master/ThirdPartyNotices-Distribution.txt) - - [ ] Run [`tpn`](https://github.com/Microsoft/vscode-python/tree/master/tpn) (typically `python tpn --npm package-lock.json --npm-overrides package.datascience-ui.dependencies.json --config tpn/distribution.toml ThirdPartyNotices-Distribution.txt`) - - [ ] Register any Python changes with [OSPO](https://opensource.microsoft.com/) -- [ ] Update [`ThirdPartyNotices-Repository.txt`](https://github.com/Microsoft/vscode-python/blob/master/ThirdPartyNotices-Repository.txt) and register any changes with OSPO -- [ ] Merge pull request into `master` -- [ ] Delete the `release` branch in the repo -- [ ] Create a new `release` branch from `master` -- [ ] Bump the version number to the next release in the `master` branch - - [ ] `package.json` - - [ ] `package-lock.json` -- [ ] Announce the code freeze is over -- [ ] Open appropriate [documentation issues](https://github.com/microsoft/vscode-docs/issues?q=is%3Aissue+is%3Aopen+label%3Apython) -- [ ] Begin drafting a [blog](http://aka.ms/pythonblog) post -- [ ] Make sure component governance is happy (requires RC PR to have been merged) - - -# Final (Tuesday, 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 -- [ ] 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) (typically `python news --final | code-insiders -`) - - Check that the "Thanks" section is up-to-date -- [ ] Update [`ThirdPartyNotices-Distribution.txt`](https://github.com/Microsoft/vscode-python/blob/master/ThirdPartyNotices-Distribution.txt) - - [ ] Run [`tpn`](https://github.com/Microsoft/vscode-python/tree/master/tpn) (typically `python tpn --npm package-lock.json --npm-overrides package.datascience-ui.dependencies.json --config tpn/distribution.toml ThirdPartyNotices-Distribution.txt`) - - [ ] Register any Python changes with component governance -- [ ] Update [`ThirdPartyNotices-Repository.txt`](https://github.com/Microsoft/vscode-python/blob/master/ThirdPartyNotices-Repository.txt) and register any changes with OSPO -- [ ] Merge pull request into `release` -- [ ] Make sure component governance is happy - -## Release - -- [ ] Make sure [CI](https://github.com/Microsoft/vscode-python/blob/master/CONTRIBUTING.md) is passing -- [ ] Generate the final `.vsix` file -- [ ] Make sure no extraneous files are being included in the `.vsix` file (make sure to check for hidden files) -- [ ] Upload the final `.vsix` file to the [marketplace](https://marketplace.visualstudio.com/items?itemName=ms-python.python) -- [ ] Publish [documentation changes](https://github.com/microsoft/vscode-docs/pulls) -- [ ] Publish the [blog](http://aka.ms/pythonblog) post -- [ ] Create a [release](https://github.com/Microsoft/vscode-python/releases) on GitHub (which creates an appropriate git tag) -- [ ] Determine if a hotfix is needed -- [ ] Merge `release` back into `master` +### 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 -- [ ] Bump the [version](https://github.com/Microsoft/vscode-python/blob/master/package.json) number to the next `alpha` -- [ ] Create a new [release plan](https://github.com/Microsoft/vscode-python/edit/master/.github/release_plan.md) -## Clean up after _this_ release -- [ ] Clean up any straggling [fixed issues needing validation](https://github.com/Microsoft/vscode-python/issues?q=label%3A%22validate+fix%22) -- [ ] Go through [`needs more info` issues](https://github.com/Microsoft/vscode-python/issues?q=is%3Aopen+label%3A%22info+needed%22+sort%3Acreated-asc) and close any that have no activity for over a month +- [ ] 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 547fc8a22f64..000000000000 --- a/.github/test_plan.md +++ /dev/null @@ -1,325 +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` - -#### 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 -```` - -**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 - -#### [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 - -#### 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 -- [ ] pep8 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 `()` - -#### [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) - -**ALWAYS**: -- Test the current debugger -- Test the experimental debugger (and note whether it is _at least_ as fast as the old debugger) - -- [ ] [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 -- [ ] [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) - -#### [`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 neccesarily 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 - -
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/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 1b56aa33068a..2fa056f84fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ .DS_Store .huskyrc.json out -node_modules +log.log +**/node_modules *.pyc +*.vsix +envVars.txt **/.vscode/.ropeproject/** **/testFiles/**/.cache/** *.noseids @@ -13,11 +16,15 @@ npm-debug.log **/.mypy_cache/** !yarn.lock coverage/ -.vscode-test/** +cucumber-report.json +**/.vscode-test/** +**/.vscode test/** +**/.vscode-smoke/** **/.venv*/ +port.txt precommit.hook -pythonFiles/experimental/ptvsd/** -pythonFiles/lib/** +python_files/lib/** +python_files/get-pip.py debug_coverage*/** languageServer/** languageServer.*/** @@ -27,3 +34,25 @@ obj/** tmp/** .python-version .vs/ +test-results*.xml +xunit-test-results.xml +build/ci/performance/performance-results.json +!build/ +debug*.log +debugpy*.log +pydevd*.log +nodeLanguageServer/** +nodeLanguageServer.*/** +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/.npmrc b/.npmrc index bc9dcc1dce60..16cc2ccdf1e8 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -@types:registry=https://registry.npmjs.org \ No newline at end of file +@types:registry=https://registry.npmjs.org diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000000..c6a66a6e6a68 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v22.21.1 diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000000..87a94b7bf466 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,15 @@ +module.exports = { + singleQuote: true, + printWidth: 120, + tabWidth: 4, + endOfLine: 'auto', + trailingComma: 'all', + overrides: [ + { + files: ['*.yml', '*.yaml'], + options: { + tabWidth: 2 + } + } + ] +}; diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 000000000000..9e466689a90a --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1,4 @@ +sonar.sources=src/client +sonar.tests=src/test +sonar.cfamily.build-wrapper-output.bypass=true +sonar.cpd.exclusions=src/client/activation/**/*.ts diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 15fd547ce708..000000000000 --- a/.travis.yml +++ /dev/null @@ -1,120 +0,0 @@ -language: python -cache: - pip: true - directories: - - $HOME/.npm - - node_modules -git: - depth: 20 - -matrix: - include: - - os: linux - python: "2.7" - env: UNIT_TEST=true - - os: linux - python: "2.7" - env: DEBUGGER_TEST_RELEASE=true - - os: linux - python: "2.7" - env: SINGLE_WORKSPACE_TEST=true - - os: linux - python: "2.7" - env: MULTIROOT_WORKSPACE_TEST=true - - os: linux - python: "3.6-dev" - env: DEBUGGER_TEST_RELEASE=true - - os: linux - python: "3.6-dev" - env: SINGLE_WORKSPACE_TEST=true - - os: linux - python: "3.6-dev" - env: MULTIROOT_WORKSPACE_TEST=true - - os: linux - python: "3.6-dev" - env: PERFORMANCE_TEST=true - - os: linux - python: "3.7-dev" - env: DEBUGGER_TEST_RELEASE=true - - os: linux - python: "3.7-dev" - env: SINGLE_WORKSPACE_TEST=true - - os: linux - python: "3.7-dev" - env: MULTIROOT_WORKSPACE_TEST=true - - os: linux - python: "3.7-dev" - env: BUNDLE=true - - os: linux - python: "3.7-dev" - env: FUNCTIONAL_TEST=true -before_install: | - if [ $TRAVIS_OS_NAME == "linux" ]; then - export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0; - sh -e /etc/init.d/xvfb start; - sleep 3; - fi - git clone https://github.com/creationix/nvm.git ./.nvm - source ./.nvm/nvm.sh - nvm install 8.9.1 - nvm use 8.9.1 - npm install npm@latest -g - npm install -g vsce - export CI_PYTHON_PATH=`which python` -install: - - travis_wait 5 npm ci - - npm run clean - - npx gulp prePublishNonBundle - - python -m pip install --upgrade pip - - python -m pip install --upgrade -r ./build/test-requirements.txt - - npx gulp installPythonLibs - -script: - - if [ $UNIT_TEST == "true" ]; then - npx gulp hygiene-modified; - npm run cover:enable; - npm run test:unittests:cover; - fi - - if [ $DEBUGGER_TEST_RELEASE == "true" ]; then - npm run cover:enable; - npm run testDebugger --silent; - fi - - npm run debugger-coverage - - if [ $FUNCTIONAL_TEST == "true" ]; then - python -m pip install --upgrade -r ./build/functional-test-requirements.txt; - npm run test:functional:cover; - fi - - if [ $SINGLE_WORKSPACE_TEST == "true" ]; then - npm run cover:enable; - npm run testSingleWorkspace --silent; - fi - - if [ $MULTIROOT_WORKSPACE_TEST == "true" ]; then - npm run cover:enable; - npm run testMultiWorkspace --silent; - fi - - if [[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" == "false" && "$PERFORMANCE_TEST" == "true" ]]; then - npm run testPerformance --silent; - fi - - if [ "$TRAVIS_PYTHON_VERSION" == "3.7" ]; then - python3 -m pip install --upgrade -r news/requirements.txt; - python3 news/announce.py --dry_run; - python3 -m pip install --upgrade -r tpn/requirements.txt; - python3 tpn --npm package-lock.json --config tpn/distribution.toml /dev/null; - fi - - if [[ $BUNDLE == "true" && $AZURE_STORAGE_ACCOUNT && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" == "false" ]]; then - npm run clean; - npm run package; - npx gulp clean:cleanExceptTests; - npm run testSmoke; - npm install -g azure-cli; - azure storage blob upload python*.vsix $AZURE_STORAGE_CONTAINER ms-python-insiders.vsix --account-name $AZURE_STORAGE_ACCOUNT --account-key $AZURE_STORAGE_ACCESS_KEY --quiet; - fi - - if [[ $BUNDLE == "true" && $AZURE_STORAGE_ACCOUNT && "$TRAVIS_BRANCH" == release* && "$TRAVIS_PULL_REQUEST" == "false" ]]; then - npm run clean; - npm run package; - npx gulp clean:cleanExceptTests; - npm run testSmoke; - npm install -g azure-cli; - azure storage blob upload python*.vsix $AZURE_STORAGE_CONTAINER ms-python-$TRAVIS_BRANCH.vsix --account-name $AZURE_STORAGE_ACCOUNT --account-key $AZURE_STORAGE_ACCESS_KEY --quiet; - fi - - bash <(curl -s https://codecov.io/bash) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index bda493d8868c..15e6aada1d50 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,7 +2,11 @@ // See https://go.microsoft.com/fwlink/?LinkId=827846 // for the documentation about the extensions.json format "recommendations": [ - "eg2.tslint", - "editorconfig.editorconfig" + "charliermarsh.ruff", + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "ms-python.python", + "ms-python.vscode-pylance" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index ab85193f044d..1e983413c8d4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,32 +7,32 @@ "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "stopOnEntry": false, + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], "smartStep": true, "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*" - ], - "preLaunchTask": "Compile" + "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"], + "env": { + // Enable this to turn on redux logging during debugging + "XVSC_PYTHON_FORCE_LOGGING": "1", + // Enable this to try out new experiments locally + "VSC_PYTHON_LOAD_EXPERIMENTS_FROM_FILE": "1", + // Enable this to log telemetry to the output during debugging + "XVSC_PYTHON_LOG_TELEMETRY": "1", + // Enable this to log debugger output. Directory must exist ahead of time + "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output_Ex" + } }, { - "name": "Debugger as debugServer", - "type": "node", + "name": "Extension inside container", + "type": "extensionHost", "request": "launch", - "program": "${workspaceFolder}/out/client/debugger/debugAdapter/main.js", - "stopOnEntry": false, + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/data"], "smartStep": true, - "args": [ - "--server=4711" - ], "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/client/**/*.js" - ], - "cwd": "${workspaceFolder}", + "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile" }, { @@ -46,56 +46,59 @@ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test" ], - "stopOnEntry": false, "sourceMaps": true, "smartStep": true, - "outFiles": [ - "${workspaceFolder}/out/**/*" - ], + "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile", "env": { "IS_CI_SERVER_TEST_DEBUGGER": "1" - } + }, + "skipFiles": ["/**"] }, { - "name": "Tests (Single Workspace, VS Code, *.test.ts)", + // Note, for the smoke test you want to debug, you may need to copy the file, + // rename it and remove a check for only smoke tests. + "name": "Tests (Smoke, VS Code, *.test.ts)", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": [ - "${workspaceFolder}/src/test", + "${workspaceFolder}/src/testMultiRootWkspc/smokeTests", "--disable-extensions", "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test" ], - "stopOnEntry": false, + "env": { + "VSC_PYTHON_CI_TEST_GREP": "Smoke Test", + "VSC_PYTHON_SMOKE_TEST": "1", + "TEST_FILES_SUFFIX": "smoke.test" + }, "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "Compile" + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] }, { - "name": "Tests (Multiroot, VS Code, *.test.ts)", + "name": "Tests (Single Workspace, VS Code, *.test.ts)", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": [ - "${workspaceFolder}/src/testMultiRootWkspc/multi.code-workspace", + "${workspaceFolder}/src/test", "--disable-extensions", "--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/**/*" - ], - "preLaunchTask": "Compile" + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] }, { - "name": "Tests (VS Code, with code coverage, *.test.ts)", + "name": "Jedi LSP tests", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", @@ -105,15 +108,33 @@ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test" ], - "stopOnEntry": false, - "smartStep": true, + "env": { + "VSC_PYTHON_CI_TEST_GREP": "Language Server:" + }, "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*.js" + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "preTestJediLSP", + "skipFiles": ["/**"] + }, + { + "name": "Tests (Multiroot, VS Code, *.test.ts)", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}/src/testMultiRootWkspc/multi.code-workspace", + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" ], "env": { - "MOCHA_REPORTER_JUNIT": "true" - } + "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**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] }, { "name": "Unit Tests (without VS Code, *.unit.test.ts)", @@ -128,13 +149,33 @@ "--ui=tdd", "--recursive", "--colors", - "--timeout=300000", - "--grep=" + //"--grep", "", + "--timeout=300000" ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] + }, + { + "name": "Unit Tests (fast, without VS Code and without react/monaco, *.unit.test.ts)", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "stopOnEntry": false, + "sourceMaps": true, + "args": [ + "./out/test/**/*.unit.test.js", + "--require=out/test/unittests.js", + "--ui=tdd", + "--recursive", + "--colors", + // "--grep", "", + "--timeout=300000", + "--fast" ], - "preLaunchTask": "Compile" + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] }, { "name": "Functional Tests (without VS Code, *.functional.test.ts)", @@ -149,31 +190,73 @@ "--ui=tdd", "--recursive", "--colors", + // "--grep", "", "--timeout=300000", - "--grep=" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" + "--exit" ], - "preLaunchTask": "Compile" + "env": { + // Remove `X` prefix to test with real browser to host DS ui (for DS functional tests). + "XVSC_PYTHON_DS_UI_BROWSER": "1", + // Remove `X` prefix to test with real python (for DS functional tests). + "XVSCODE_PYTHON_ROLLING": "1", + // Remove 'X' to turn on all logging in the debug output + "XVSC_PYTHON_FORCE_LOGGING": "1", + // Remove `X` prefix and update path to test with real python interpreter (for DS functional tests). + "XCI_PYTHON_PATH": "", + // Remove 'X' prefix to dump output for debugger. Directory has to exist prior to launch + "XDEBUGPY_LOG_DIR": "${workspaceRoot}/tmp/Debug_Output", + // 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**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] }, { "type": "node", "request": "launch", "name": "Gulp tasks (helpful for debugging gulpfile.js)", "program": "${workspaceFolder}/node_modules/gulp/bin/gulp.js", - "args": [ - "watch" - ] + "args": ["watch"], + "skipFiles": ["/**"] + }, + { + "name": "Node: Current File", + "program": "${file}", + "request": "launch", + "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": "Extension + Debugger", - "configurations": [ - "Extension", - "Debugger as debugServer" - ] + "name": "Debug Python and Extension", + "configurations": ["Python: Attach Listen", "Extension"] } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index d928b65c7554..01de0d907706 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,25 +2,77 @@ { "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, "bin": true, "**/__pycache__": true, - "node_modules": true, - ".vscode-test": true, - "**/.mypy_cache/**": true, - "**/.ropeproject/**": true + "**/node_modules": true, + ".vscode-test": false, + ".vscode test": false, + "**/.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 + "languageServer*/**": true, + ".vscode-test": true, + ".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.unitTest.promptToConfigure": false, - "python.workspaceSymbols.enabled": false, - "python.formatting.provider": "none" + "typescript.preferences.quoteStyle": "single", + "javascript.preferences.quoteStyle": "single", + "prettier.printWidth": 120, + "prettier.singleQuote": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "python.languageServer": "Default", + "typescript.preferences.importModuleSpecifier": "relative", + // 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 58c524b0df0f..c5a054ed43cf 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,29 +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": [] - }, { "label": "Run Unit Tests", "type": "npm", @@ -45,99 +28,34 @@ } }, { - "label": "Hygiene", - "type": "gulp", - "task": "watch", - "isBackground": true, - "presentation": { - "echo": true, - "reveal": "never", - "focus": false, - "panel": "dedicated" - }, - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": [] + "type": "npm", + "script": "preTestJediLSP", + "problemMatcher": [], + "label": "preTestJediLSP" }, { - "label": "Hygiene Watch Branch", - "type": "gulp", - "task": "hygiene-watch-branch", - "isBackground": true, - "presentation": { - "echo": true, - "reveal": "never", - "focus": false, - "panel": "dedicated" - }, - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": [] + "type": "npm", + "script": "check-python", + "problemMatcher": ["$python"], + "label": "npm: check-python", + "detail": "npm run check-python:ruff && npm run check-python:pyright" }, { - "label": "Hygiene (Problems Window)", - "type": "gulp", - "task": "watchProblems", - "isBackground": true, - "presentation": { - "echo": true, - "reveal": "never", - "focus": false, - "panel": "dedicated" - }, - "problemMatcher": [ - { - "applyTo": "allDocuments", - "fileLocation": "relative", - "background": { - "beginsPattern": { - "regexp": "^Hygiene started" - }, - "endsPattern": { - "regexp": "^(Hygiene failed with errors|Hygiene passed with 0 errors)" - } - }, - "pattern": [ - { - "regexp": "^([^\\s].*)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\):\\s+(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$", - "file": 1, - "location": 2, - "severity": 3, - "code": 4, - "message": 5 - } - ] - }, - { - "applyTo": "allDocuments", - "fileLocation": "relative", - "background": { - "beginsPattern": { - "regexp": "^Hygiene started" - }, - "endsPattern": { - "regexp": "^(Hygiene failed with errors|Hygiene passed with 0 errors)" - } - }, - "pattern": [ - { - "regexp": "^(WARNING|ERROR):(\\s+\\(\\S*\\))?\\s+(\\S.*)\\[(\\d+), (\\d+)\\]:\\s+(.*)$", - "severity": 1, - "file": 3, - "line": 4, - "column": 5, - "message": 6 - } - ] - } - ], - "group": { - "kind": "build", - "isDefault": true + "label": "npm: check-python (venv)", + "type": "shell", + "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 5761f3dc684f..d636ab48f361 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,69 +1,89 @@ -!out/client/**/*.map **/*.map +**/*.analyzer.html +**/.env *.vsix -.appveyor.yml .editorconfig +.env .eslintrc +.eslintignore .gitattributes .gitignore .gitmodules -.huskyrc.json +.git* .npmrc -.travis.yml +.nvmrc +.nycrc CODE_OF_CONDUCT.md CODING_STANDARDS.md CONTRIBUTING.md -CONTRIBUTING - LANGUAGE SERVER.md -coverconfig.json gulpfile.js -package.datascience-ui.dependencies.json package-lock.json -packageExtension.cmd -pvsc-dev-ext.py -PYTHON_INTERACTIVE_TROUBLESHOOTING.md +requirements.in +sprint-planning.github-issues +test.ipynb tsconfig*.json tsfmt.json -tslint.json -typings.json -vsc-extension-quickstart.md vscode-python-signing.* -webpack.config.js -webpack.datascience-ui.config.js +noxfile.py +.config/** .github/** .mocha-reporter/** .nvm/** +.nox/** .nyc_output +.prettierrc.js +.sonarcloud.properties .venv/** .vscode/** .vscode-test/** +.vscode test/** languageServer/** languageServer.*/** +nodeLanguageServer/** +nodeLanguageServer.*/** bin/** build/** BuildOutput/** coverage/** +data/** debug_coverage*/** images/**/*.gif images/**/*.png -news/** +ipywidgets/** +i18n/** node_modules/** obj/** -out/client/**/*analyzer.html +out/**/*.stats.json +out/client/**/*.analyzer.html out/coverconfig.json -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/** -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/** -tpn/** 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 b8180e9b0bb1..56c1f7697ad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8980 @@ # Changelog +**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 + +1. Support connecting to Jupyter hub servers. Use either the base url of the server (i.e. 'https://111.11.11.11:8000') or your user folder (i.e. 'https://111.11.11.11:8000/user/theuser). + Works with password authentication. + ([#9679](https://github.com/Microsoft/vscode-python/issues/9679)) +1. Added "argsExpansion" to debugpy launch.json schema. + ([#11678](https://github.com/Microsoft/vscode-python/issues/11678)) +1. The extension will now automatically load if a `pyproject.toml` file is present in the workspace root directory. + (thanks [Brandon White](https://github.com/BrandonLWhite)) + ([#12056](https://github.com/Microsoft/vscode-python/issues/12056)) +1. Add ability to check and update whether a notebook is trusted. + ([#12146](https://github.com/Microsoft/vscode-python/issues/12146)) +1. Support formatting of Notebook Cells when using the VS Code Insiders API for Notebooks. + ([#12195](https://github.com/Microsoft/vscode-python/issues/12195)) +1. Added exporting notebooks to HTML. + ([#12375](https://github.com/Microsoft/vscode-python/issues/12375)) +1. Change stock launch.json "attach" config to use "connect". + ([#12446](https://github.com/Microsoft/vscode-python/issues/12446)) +1. Update to the latest version of [`jedi`](https://github.com/davidhalter/jedi) (`0.17.1`). This brings completions for Django (via [`django-stubs`](https://github.com/typeddjango/django-stubs)) as well as support for Python 3.9 and various bugfixes (mostly around generic type annotations). (thanks [Peter Law](https://gitlab.com/PeterJCLaw/)) + ([#12486](https://github.com/Microsoft/vscode-python/issues/12486)) +1. Prompt users that we have deleted pythonPath from their workspace settings when in `Deprecate PythonPath` experiment. + ([#12533](https://github.com/Microsoft/vscode-python/issues/12533)) +1. Changed public API for execution to return an object and provide a callback which is called when interpreter setting changes. + ([#12596](https://github.com/Microsoft/vscode-python/issues/12596)) +1. Allow users to opt out of us checking whether their notebooks can be trusted. This setting is turned off by default and must be manually enabled. + ([#12611](https://github.com/Microsoft/vscode-python/issues/12611)) +1. Include the JUPYTER_PATH environment variable when searching the disk for kernels. + ([#12694](https://github.com/Microsoft/vscode-python/issues/12694)) +1. Added exporting to python, HTML and PDF from the interactive window. + ([#12732](https://github.com/Microsoft/vscode-python/issues/12732)) +1. Show a prompt asking user to upgrade Code runner to new version to keep using it when in Deprecate PythonPath experiment. + ([#12764](https://github.com/Microsoft/vscode-python/issues/12764)) +1. Opening notebooks in the preview Notebook editor for [Visual Studio Code Insiders](https://code.visualstudio.com/insiders/). + ([#10496](https://github.com/Microsoft/vscode-python/issues/10496)) + +### Fixes + +1. Ensure we only have a single isort process running on a single file. + ([#10579](https://github.com/Microsoft/vscode-python/issues/10579)) +1. Provided a method for external partners to participate in jupyter server URI picking/authentication. + ([#10993](https://github.com/Microsoft/vscode-python/issues/10993)) +1. Check for hideFromUser before activating current terminal. + ([#11122](https://github.com/Microsoft/vscode-python/issues/11122)) +1. In Markdown cells, turn HTML links to markdown links so that nteract renders them. + ([#11254](https://github.com/Microsoft/vscode-python/issues/11254)) +1. Prevent incorrect ipywidget display (double plots) due to synchronization issues. + ([#11281](https://github.com/Microsoft/vscode-python/issues/11281)) +1. Removed the Kernel Selection toolbar from the Interactive Window when using a local Jupyter Server. + To show it again, set the setting 'Python > Data Science > Show Kernel Selection On Interactive Window'. + ([#11347](https://github.com/Microsoft/vscode-python/issues/11347)) +1. Get Jupyter connections to work with a Windows store installed Python/Jupyter combination. + ([#11412](https://github.com/Microsoft/vscode-python/issues/11412)) +1. Disable hover intellisense in the interactive window unless the code is expanded. + ([#11459](https://github.com/Microsoft/vscode-python/issues/11459)) +1. Make layout of markdown editors much faster to open. + ([#11584](https://github.com/Microsoft/vscode-python/issues/11584)) +1. Watermark in the interactive window can appear on top of entered text. + ([#11691](https://github.com/Microsoft/vscode-python/issues/11691)) +1. Jupyter can fail to run a kernel if the user's environment contains non string values. + ([#11749](https://github.com/Microsoft/vscode-python/issues/11749)) +1. On Mac meta+Z commands are performing both cell and editor undos. + ([#11758](https://github.com/Microsoft/vscode-python/issues/11758)) +1. Paste can sometimes double paste into a notebook or interactive window editor. + ([#11796](https://github.com/Microsoft/vscode-python/issues/11796)) +1. Fix jupyter connections going down when azure-storage or other extensions with node-fetch are installed. + ([#11830](https://github.com/Microsoft/vscode-python/issues/11830)) +1. Variables should not flash when running by line. + ([#12046](https://github.com/Microsoft/vscode-python/issues/12046)) +1. Discard changes on Notebooks when the user selects 'Don't Save' on the save changes dialog. + ([#12180](https://github.com/Microsoft/vscode-python/issues/12180)) +1. Disable `Extract variable & method` commands in `Notebook Cells`. + ([#12206](https://github.com/Microsoft/vscode-python/issues/12206)) +1. Disable linting in Notebook Cells. + ([#12208](https://github.com/Microsoft/vscode-python/issues/12208)) +1. Register services before extension activates. + ([#12227](https://github.com/Microsoft/vscode-python/issues/12227)) +1. Infinite loop of asking to reload the extension when enabling custom editor. + ([#12231](https://github.com/Microsoft/vscode-python/issues/12231)) +1. Fix raw kernel autostart and remove jupyter execution from interactive base. + ([#12330](https://github.com/Microsoft/vscode-python/issues/12330)) +1. If we fail to start a raw kernel daemon then fall back to using process execution. + ([#12355](https://github.com/Microsoft/vscode-python/issues/12355)) +1. Fix the export button from the interactive window to export again. + ([#12460](https://github.com/Microsoft/vscode-python/issues/12460)) +1. Process Jupyter messages synchronously when possible. + ([#12588](https://github.com/Microsoft/vscode-python/issues/12588)) +1. Open variable explorer when opening variable explorer during debugging. + ([#12773](https://github.com/Microsoft/vscode-python/issues/12773)) +1. Use the given interpreter for launching the non-daemon python + ([#12821](https://github.com/Microsoft/vscode-python/issues/12821)) +1. Correct the color of the 'Collapse All' button in the Interactive Window + ([#12838](https://github.com/microsoft/vscode-python/issues/12838)) + +### Code Health + +1. Move all logging to the Python output channel. + ([#9837](https://github.com/Microsoft/vscode-python/issues/9837)) +1. Add a functional test that opens both the interactive window and a notebook at the same time. + ([#11445](https://github.com/Microsoft/vscode-python/issues/11445)) +1. Added setting `python.logging.level` which carries the logging level value the extension will log at. + ([#11699](https://github.com/Microsoft/vscode-python/issues/11699)) +1. Monkeypatch `console.*` calls to the logger only in CI. + ([#11896](https://github.com/Microsoft/vscode-python/issues/11896)) +1. Replace python.dataScience.ptvsdDistPath with python.dataScience.debugpyDistPath. + ([#11993](https://github.com/Microsoft/vscode-python/issues/11993)) +1. Rename ptvsd to debugpy in Telemetry. + ([#11996](https://github.com/Microsoft/vscode-python/issues/11996)) +1. Update JSDoc annotations for many of the APIs (thanks [Anthony Shaw](https://github.com/tonybaloney)) + ([#12101](https://github.com/Microsoft/vscode-python/issues/12101)) +1. Refactor `LinterId` to an enum instead of a string union. + (thanks to [Anthony Shaw](https://github.com/tonybaloney)) + ([#12116](https://github.com/Microsoft/vscode-python/issues/12116)) +1. Remove webserver used to host contents in WebViews. + ([#12140](https://github.com/Microsoft/vscode-python/issues/12140)) +1. Inline interface due to issues with custom types when using `ts-node`. + ([#12238](https://github.com/Microsoft/vscode-python/issues/12238)) +1. Fix linux nightly tests so they run and report results. Also seems to get rid of stream destroyed messages for raw kernel. + ([#12539](https://github.com/Microsoft/vscode-python/issues/12539)) +1. Log ExP experiments the user belongs to in the output panel. + ([#12656](https://github.com/Microsoft/vscode-python/issues/12656)) +1. Add more telemetry for "Select Interpreter" command. + ([#12722](https://github.com/Microsoft/vscode-python/issues/12722)) +1. Add tests for trusted notebooks. + ([#12554](https://github.com/Microsoft/vscode-python/issues/12554)) +1. Update categories in `package.json`. + ([#12844](https://github.com/Microsoft/vscode-python/issues/12844)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.6.3 (30 June 2020) + +### Fixes + +1. Correctly check for ZMQ support, previously it could allow ZMQ to be supported when zmq could not be imported. + ([#12585](https://github.com/Microsoft/vscode-python/issues/12585)) +1. Auto indentation no longer working for notebooks and interactive window. + ([#12389](https://github.com/Microsoft/vscode-python/issues/12389)) +1. Add telemetry for tracking run by line. + ([#12580](https://github.com/Microsoft/vscode-python/issues/12580)) +1. Add more telemetry to distinguish how is the start page opened. + ([#12603](https://github.com/microsoft/vscode-python/issues/12603)) +1. Stop looking for mspythonconfig.json file in subfolders. + ([#12614](https://github.com/Microsoft/vscode-python/issues/12614)) + +## 2020.6.2 (25 June 2020) + +### Fixes + +1. Fix `linting.pylintEnabled` setting check. + ([#12285](https://github.com/Microsoft/vscode-python/issues/12285)) +1. Don't modify LS settings if jediEnabled does not exist. + ([#12429](https://github.com/Microsoft/vscode-python/issues/12429)) + +## 2020.6.1 (17 June 2020) + +### Fixes + +1. Fixed issue when `python.jediEnabled` setting was not removed and `python.languageServer` setting was not updated. + ([#12429](https://github.com/Microsoft/vscode-python/issues/12429)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.6.0 (16 June 2020) + +### Enhancements + +1. Removed `python.jediEnabled` setting in favor of `python.languageServer`. Instead of `"python.jediEnabled": true` please use `"python.languageServer": "Jedi"`. + ([#7010](https://github.com/Microsoft/vscode-python/issues/7010)) +1. Added a start page for the extension. It opens to new users or when there is a new release. It can be disabled with the setting 'Python: Show Start Page'. + ([#11057](https://github.com/Microsoft/vscode-python/issues/11057)) +1. Preliminary support using other languages for the kernel. + ([#11919](https://github.com/Microsoft/vscode-python/issues/11919)) +1. Enable the use of the custom editor for native notebooks. + ([#10744](https://github.com/Microsoft/vscode-python/issues/10744)) + +### Fixes + +1. Ensure sorting imports in a modified file picks up the proper configuration. + thanks [Peter Law](https://github.com/PeterJCLaw)) + ([#4891](https://github.com/Microsoft/vscode-python/issues/4891)) +1. Made variable explorer (from IPython Notebook interface) resizable. + ([#5382](https://github.com/Microsoft/vscode-python/issues/5382)) +1. Add junit family to pytest runner args to remove pytest warning. + ([#10673](https://github.com/Microsoft/vscode-python/issues/10673)) +1. Switch order of restart and cancel buttons in interactive window to be consistent with ordering in notebook toolbar. + ([#11091](https://github.com/Microsoft/vscode-python/issues/11091)) +1. Support opening other URI schemes besides 'file' and 'vsls'. + ([#11393](https://github.com/Microsoft/vscode-python/issues/11393)) +1. Fix issue with formatting when the first line is blank. + ([#11416](https://github.com/Microsoft/vscode-python/issues/11416)) +1. Force interactive window to always scroll long output. Don't allow scrollbars within scrollbars. + ([#11421](https://github.com/Microsoft/vscode-python/issues/11421)) +1. Hover on notebooks or interactive window seems to stutter. + ([#11422](https://github.com/Microsoft/vscode-python/issues/11422)) +1. Make shift+tab work again in the interactive window. Escaping focus from the prompt is now relegated to 'Shift+Esc'. + ([#11495](https://github.com/Microsoft/vscode-python/issues/11495)) +1. Keep import and export working with raw kernel mode. Also allow for installing dependencies if running an import before jupyter was ever launched. + ([#11501](https://github.com/Microsoft/vscode-python/issues/11501)) +1. Extra kernels that just say "Python 3 - python" are showing up in the raw kernel kernel picker. + ([#11552](https://github.com/Microsoft/vscode-python/issues/11552)) +1. Fix intermittent launch failure with raw kernels on windows. + ([#11574](https://github.com/Microsoft/vscode-python/issues/11574)) +1. Don't register a kernelspec when switching to an interpreter in raw kernel mode. + ([#11575](https://github.com/Microsoft/vscode-python/issues/11575)) +1. Keep the notebook input prompt up if you focus out of vscode. + ([#11581](https://github.com/Microsoft/vscode-python/issues/11581)) +1. Fix install message to reference run by line instead of debugging. + ([#11661](https://github.com/Microsoft/vscode-python/issues/11661)) +1. Run by line does not scroll to the line that is being run. + ([#11662](https://github.com/Microsoft/vscode-python/issues/11662)) +1. For direct kernel connection, don't replace a notebook's metadata default kernelspec with a new kernelspec on startup. + ([#11672](https://github.com/Microsoft/vscode-python/issues/11672)) +1. Fixes issue with importing `debupy` in interactive window. + ([#11686](https://github.com/Microsoft/vscode-python/issues/11686)) +1. Reopen all notebooks when rerunning the extension (including untitled ones). + ([#11711](https://github.com/Microsoft/vscode-python/issues/11711)) +1. Make sure to clear 'outputPrepend' when rerunning cells and to also only ever add it once to a cell. + (thanks [Barry Nolte](https://github.com/BarryNolte)) + ([#11726](https://github.com/Microsoft/vscode-python/issues/11726)) +1. Disable pre-warming of Kernel Daemons when user does not belong to the `LocalZMQKernel - experiment` experiment. + ([#11751](https://github.com/Microsoft/vscode-python/issues/11751)) +1. When switching to an invalid kernel (one that is registered but cannot start) in raw mode respect the launch timeout that is passed in. + ([#11752](https://github.com/Microsoft/vscode-python/issues/11752)) +1. Make `python.dataScience.textOutputLimit` apply on subsequent rerun. We were letting the 'outputPrepend' metadata persist from run to run. + (thanks [Barry Nolte](https://github.com/BarryNolte)) + ([#11777](https://github.com/Microsoft/vscode-python/issues/11777)) +1. Use `${command:python.interpreterPath}` to get selected interpreter path in `launch.json` and `tasks.json`. + ([#11789](https://github.com/Microsoft/vscode-python/issues/11789)) +1. Restarting a kernel messes up run by line. + ([#11793](https://github.com/Microsoft/vscode-python/issues/11793)) +1. Correctly show kernel status in raw kernel mode. + ([#11797](https://github.com/Microsoft/vscode-python/issues/11797)) +1. Hovering over variables in a python file can show two hover values if the interactive window is closed and reopened. + ([#11800](https://github.com/Microsoft/vscode-python/issues/11800)) +1. Make sure to use webView.cspSource for all csp sources. + ([#11855](https://github.com/Microsoft/vscode-python/issues/11855)) +1. Use command line arguments to launch our raw kernels as opposed to a connection file. The connection file seems to be causing issues in particular on windows CI machines with permissions. + ([#11883](https://github.com/Microsoft/vscode-python/issues/11883)) +1. Improve our status reporting when launching and connecting to a raw kernel. + ([#11951](https://github.com/Microsoft/vscode-python/issues/11951)) +1. Prewarm raw kernels based on raw kernel support and don't prewarm if jupyter autostart is disabled. + ([#11956](https://github.com/Microsoft/vscode-python/issues/11956)) +1. Don't flood the hard drive when typing in a large notebook file. + ([#12058](https://github.com/Microsoft/vscode-python/issues/12058)) +1. Disable run-by-line and continue buttons in run by line mode when running. + ([#12169](https://github.com/Microsoft/vscode-python/issues/12169)) +1. Disable `Sort Imports` command in `Notebook Cells`. + ([#12193](https://github.com/Microsoft/vscode-python/issues/12193)) +1. Fix debugger continue event to actually change a cell. + ([#12155](https://github.com/Microsoft/vscode-python/issues/12155)) +1. Make Jedi the Default value for the python.languageServer setting. + ([#12225](https://github.com/Microsoft/vscode-python/issues/12225)) +1. Make stop during run by line interrupt the kernel. + ([#12249](https://github.com/Microsoft/vscode-python/issues/12249)) +1. Have raw kernel respect the jupyter server disable auto start setting. + ([#12246](https://github.com/Microsoft/vscode-python/issues/12246)) + +### Code Health + +1. Use ts-loader as a tyepscript loader in webpack. + ([#9061](https://github.com/Microsoft/vscode-python/issues/9061)) +1. Fixed typo from unitest -> unittest. + (thanks [Rameez Khan](https://github.com/Rxmeez)). + ([#10919](https://github.com/Microsoft/vscode-python/issues/10919)) +1. Make functional tests more deterministic. + ([#11058](https://github.com/Microsoft/vscode-python/issues/11058)) +1. Reenable CDN unit tests. + ([#11442](https://github.com/Microsoft/vscode-python/issues/11442)) +1. Run by line for notebook cells minimal implementation. + ([#11607](https://github.com/Microsoft/vscode-python/issues/11607)) +1. Get shape and count when showing debugger variables. + ([#11657](https://github.com/Microsoft/vscode-python/issues/11657)) +1. Add more tests to verify data frames can be opened. + ([#11658](https://github.com/Microsoft/vscode-python/issues/11658)) +1. Support data tips overtop of python files that have had cells run. + ([#11659](https://github.com/Microsoft/vscode-python/issues/11659)) +1. Functional test for run by line functionality. + ([#11660](https://github.com/Microsoft/vscode-python/issues/11660)) +1. Fixed typo in a test from lanaguage -> language. + (thanks [Ashwin Ramaswami](https://github.com/epicfaace)). + ([#11775](https://github.com/Microsoft/vscode-python/issues/11775)) +1. Add bitness information to interpreter telemetry. + ([#11904](https://github.com/Microsoft/vscode-python/issues/11904)) +1. Fix failing linux debugger tests. + ([#11935](https://github.com/Microsoft/vscode-python/issues/11935)) +1. Faster unit tests on CI Pipeline. + ([#12017](https://github.com/Microsoft/vscode-python/issues/12017)) +1. Ensure we can use proposed VS Code API with `ts-node`. + ([#12025](https://github.com/Microsoft/vscode-python/issues/12025)) +1. Faster node unit tests on Azure pipeline. + ([#12027](https://github.com/Microsoft/vscode-python/issues/12027)) +1. Use [deemon](https://www.npmjs.com/package/deemon) package for background compilation with support for restarting VS Code during development. + ([#12059](https://github.com/Microsoft/vscode-python/issues/12059)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.5.3 (10 June 2020) + +1. Update `debugpy` to use `1.0.0b11` or greater. + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.5.2 (8 June 2020) + +### Fixes + +1. Double-check for interpreters when running diagnostics before displaying the "Python is not installed" message. + ([#11870](https://github.com/Microsoft/vscode-python/issues/11870)) +1. Ensure user cannot belong to all experiments in an experiment group. + ([#11943](https://github.com/Microsoft/vscode-python/issues/11943)) +1. Ensure extension features are started when in `Deprecate PythonPath` experiment and opening a file without any folder opened. + ([#12177](https://github.com/Microsoft/vscode-python/issues/12177)) + +### Code Health + +1. Integrate VS Code experiment framework in the extension. + ([#10790](https://github.com/Microsoft/vscode-python/issues/10790)) +1. Update telemetry on errors and exceptions to use [vscode-extension-telemetry](https://www.npmjs.com/package/vscode-extension-telemetry). + ([#11597](https://github.com/Microsoft/vscode-python/issues/11597)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.5.1 (19 May 2020) + +### Fixes + +1. Do not execute shebang as an interpreter until user has clicked on the codelens enclosing the shebang. + ([#11687](https://github.com/Microsoft/vscode-python/issues/11687)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.5.0 (12 May 2020) + +### Enhancements + +1. Added ability to manually enter a path to interpreter in the select interpreter dropdown. + ([#216](https://github.com/Microsoft/vscode-python/issues/216)) +1. Add status bar item with icon when installing Insiders/Stable build. + (thanks to [ErwanDL](https://github.com/ErwanDL/)) + ([#10495](https://github.com/Microsoft/vscode-python/issues/10495)) +1. Support for language servers that don't allow incremental document updates inside of notebooks and the interactive window. + ([#10818](https://github.com/Microsoft/vscode-python/issues/10818)) +1. Add telemetry for "Python is not installed" prompt. + ([#10885](https://github.com/Microsoft/vscode-python/issues/10885)) +1. Add basic liveshare support for raw kernels. + ([#10988](https://github.com/Microsoft/vscode-python/issues/10988)) +1. Do a one-off transfer of existing values for `python.pythonPath` setting to new Interpreter storage if in DeprecatePythonPath experiment. + ([#11052](https://github.com/Microsoft/vscode-python/issues/11052)) +1. Ensure the language server can query pythonPath when in the Deprecate PythonPath experiment. + ([#11083](https://github.com/Microsoft/vscode-python/issues/11083)) +1. Added prompt asking users to delete `python.pythonPath` key from their workspace settings when in Deprecate PythonPath experiment. + ([#11108](https://github.com/Microsoft/vscode-python/issues/11108)) +1. Added `getDebuggerPackagePath` extension API to get the debugger package path. + ([#11236](https://github.com/Microsoft/vscode-python/issues/11236)) +1. Expose currently selected interpreter path using API. + ([#11294](https://github.com/Microsoft/vscode-python/issues/11294)) +1. Show a prompt asking user to upgrade Code runner to new version to keep using it when in Deprecate PythonPath experiment. + ([#11327](https://github.com/Microsoft/vscode-python/issues/11327)) +1. Rename string `${config:python.pythonPath}` which is used in `launch.json` to refer to interpreter path set in settings, to `${config:python.interpreterPath}`. + ([#11446](https://github.com/Microsoft/vscode-python/issues/11446)) + +### Fixes + +1. Added 'Enable Scrolling For Cell Outputs' setting. Works together with the 'Max Output Size' setting. + ([#9801](https://github.com/Microsoft/vscode-python/issues/9801)) +1. Fix ctrl+enter on markdown cells. Now they render. + ([#10006](https://github.com/Microsoft/vscode-python/issues/10006)) +1. Cancelling the prompt to restart the kernel should not leave the toolbar buttons disabled. + ([#10356](https://github.com/Microsoft/vscode-python/issues/10356)) +1. Getting environment variables of activated environments should ignore the setting `python.terminal.activateEnvironment`. + ([#10370](https://github.com/Microsoft/vscode-python/issues/10370)) +1. Show notebook path when listing remote kernels. + ([#10521](https://github.com/Microsoft/vscode-python/issues/10521)) +1. Allow filtering on '0' for the Data Viewer. + ([#10552](https://github.com/Microsoft/vscode-python/issues/10552)) +1. Allow interrupting the kernel more than once. + ([#10587](https://github.com/Microsoft/vscode-python/issues/10587)) +1. Make error links in exception tracebacks support multiple cells in the stack and extra spaces. + ([#10708](https://github.com/Microsoft/vscode-python/issues/10708)) +1. Add channel property onto returned ZMQ messages. + ([#10785](https://github.com/Microsoft/vscode-python/issues/10785)) +1. Fix problem with shape not being computed for some types in the variable explorer. + ([#10825](https://github.com/Microsoft/vscode-python/issues/10825)) +1. Enable cell related commands when a Python file is already open. + ([#10884](https://github.com/Microsoft/vscode-python/issues/10884)) +1. Fix issue with parsing long conda environment names. + ([#10942](https://github.com/Microsoft/vscode-python/issues/10942)) +1. Hide progress indicator once `Interactive Window` has loaded. + ([#11065](https://github.com/Microsoft/vscode-python/issues/11065)) +1. Do not perform pipenv interpreter discovery on extension activation. + Fix for [CVE-2020-1171](https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2020-1171). + ([#11127](https://github.com/Microsoft/vscode-python/issues/11127)) +1. Ensure arguments are included in log messages when using decorators. + ([#11153](https://github.com/Microsoft/vscode-python/issues/11153)) +1. Fix for opening the interactive window when no workspace is open. + ([#11291](https://github.com/Microsoft/vscode-python/issues/11291)) +1. Conda environments working with raw kernels. + ([#11306](https://github.com/Microsoft/vscode-python/issues/11306)) +1. Ensure isolate script is passed as command argument when installing modules. + ([#11399](https://github.com/Microsoft/vscode-python/issues/11399)) +1. Make raw kernel launch respect launched resource environment. + ([#11451](https://github.com/Microsoft/vscode-python/issues/11451)) +1. When using a kernelspec without a fully qualified python path make sure we use the resource to get the active interpreter. + ([#11469](https://github.com/Microsoft/vscode-python/issues/11469)) +1. For direct kernel launch correctly detect if interpreter has changed since last launch. + ([#11530](https://github.com/Microsoft/vscode-python/issues/11530)) +1. Performance improvements when executing multiple cells in `Notebook` and `Interactive Window`. + ([#11576](https://github.com/Microsoft/vscode-python/issues/11576)) +1. Ensure kernel daemons are disposed correctly when closing notebooks. + ([#11579](https://github.com/Microsoft/vscode-python/issues/11579)) +1. When VS quits, make sure to save contents of notebook for next reopen. + ([#11557](https://github.com/Microsoft/vscode-python/issues/11557)) +1. Fix scrolling when clicking in the interactive window to not jump around. + ([#11554](https://github.com/Microsoft/vscode-python/issues/11554)) +1. Setting "Data Science: Run Startup Commands" is now limited to being a user setting. + Fix for [CVE-2020-1192](https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2020-1192). + +### Code Health + +1. Enable the `Self Cert` tests for Notebooks. + ([#10447](https://github.com/Microsoft/vscode-python/issues/10447)) +1. Remove deprecated telemetry and old way of searching for `Jupyter`. + ([#10809](https://github.com/Microsoft/vscode-python/issues/10809)) +1. Add telemetry for pipenv interpreter discovery. + ([#11128](https://github.com/Microsoft/vscode-python/issues/11128)) +1. Update to the latest version of [`jedi`](https://github.com/davidhalter/jedi) (`0.17`). Note that this may be the last version of Jedi to support Python 2 and Python 3.5. (#11221; thanks Peter Law) + ([#11221](https://github.com/Microsoft/vscode-python/issues/11221)) +1. Lazy load types from `jupyterlab/services` and similar `npm modules`. + ([#11297](https://github.com/Microsoft/vscode-python/issues/11297)) +1. Remove IJMPConnection implementation while maintaining tests written for it. + ([#11470](https://github.com/Microsoft/vscode-python/issues/11470)) +1. Implement an IJupyterVariables provider for the debugger. + ([#11542](https://github.com/Microsoft/vscode-python/issues/11542)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.4.1 (27 April 2020) + +### Fixes + +1. Use node FS APIs when searching for python. This is a temporary change until VSC FS APIs are fixed. + ([#10850](https://github.com/Microsoft/vscode-python/issues/10850)) +1. Show unhandled widget messages in the jupyter output window. + ([#11239](https://github.com/Microsoft/vscode-python/issues/11239)) +1. Warn when using a version of the widget `qgrid` greater than `1.1.1` with the recommendation to downgrade to `1.1.1`. + ([#11245](https://github.com/Microsoft/vscode-python/issues/11245)) +1. Allow user modules import when discovering tests. + ([#11264](https://github.com/Microsoft/vscode-python/issues/11264)) +1. Fix issue where downloading ipywidgets from the CDN might be busy. + ([#11274](https://github.com/Microsoft/vscode-python/issues/11274)) +1. Error: Timeout is shown after running any widget more than once. + ([#11334](https://github.com/Microsoft/vscode-python/issues/11334)) +1. Change "python.dataScience.runStartupCommands" commands to be a global setting, not a workspace setting. + ([#11352](https://github.com/Microsoft/vscode-python/issues/11352)) +1. Closing the interactive window shuts down other active notebook sessions. + ([#11404](https://github.com/Microsoft/vscode-python/issues/11404)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.4.0 (20 April 2020) + +### Enhancements + +1. Add support for `ipywidgets`. + ([#3429](https://github.com/Microsoft/vscode-python/issues/3429)) +1. Support output and interact ipywidgets. + ([#9524](https://github.com/Microsoft/vscode-python/issues/9524)) +1. Support using 'esc' or 'ctrl+u' to clear the contents of the interactive window input box. + ([#10198](https://github.com/Microsoft/vscode-python/issues/10198)) +1. Use new interpreter storage supporting multiroot workspaces when in Deprecate PythonPath experiment. + ([#10325](https://github.com/Microsoft/vscode-python/issues/10325)) +1. Modified `Select interpreter` command to support setting interpreter at workspace level. + ([#10372](https://github.com/Microsoft/vscode-python/issues/10372)) +1. Added a command `Clear Workspace Interpreter Setting` to clear value of Python interpreter from workspace settings. + ([#10374](https://github.com/Microsoft/vscode-python/issues/10374)) +1. Support reverse connection ("listen" in launch.json) from debug adapter to VSCode. + ([#10437](https://github.com/Microsoft/vscode-python/issues/10437)) +1. Use specific icons when downloading MPLS and Insiders instead of the spinner. + ([#10495](https://github.com/Microsoft/vscode-python/issues/10495)) +1. Notebook metadata is now initialized in alphabetical order. + ([#10571](https://github.com/Microsoft/vscode-python/issues/10571)) +1. Added command translations for Hindi Language. + (thanks [Pai026](https://github.com/Pai026/)) + ([#10711](https://github.com/Microsoft/vscode-python/issues/10711)) +1. Prompt when an "untrusted" workspace Python environment is to be auto selected when in Deprecate PythonPath experiment. + ([#10879](https://github.com/Microsoft/vscode-python/issues/10879)) +1. Added a command `Reset stored info for untrusted Interpreters` to reset "untrusted" interpreters storage when in Deprecate PythonPath experiment. + ([#10912](https://github.com/Microsoft/vscode-python/issues/10912)) +1. Added a user setting `python.defaultInterpreterPath` to set up the default interpreter path when in Deprecate PythonPath experiment. + ([#11021](https://github.com/Microsoft/vscode-python/issues/11021)) +1. Hide "untrusted" interpreters from 'Select interpreter' dropdown list when in DeprecatePythonPath Experiment. + ([#11046](https://github.com/Microsoft/vscode-python/issues/11046)) +1. Make spacing of icons on notebook toolbars match spacing on other VS code toolbars. + ([#10464](https://github.com/Microsoft/vscode-python/issues/10464)) +1. Make jupyter server status centered in the UI and use the same font as the rest of VS code. + ([#10465](https://github.com/Microsoft/vscode-python/issues/10465)) +1. Performa validation of interpreter only when a Notebook is opened instead of when extension activates. + ([#10893](https://github.com/Microsoft/vscode-python/issues/10893)) +1. Scrolling in cells doesn't happen on new line. + ([#10952](https://github.com/Microsoft/vscode-python/issues/10952)) +1. Ensure images in workspace folder are supported within markdown cells in a `Notebook`. + ([#11040](https://github.com/Microsoft/vscode-python/issues/11040)) +1. Make sure ipywidgets have a white background so they display in dark themes. + ([#11060](https://github.com/Microsoft/vscode-python/issues/11060)) +1. Arrowing down through cells put the cursor in the wrong spot. + ([#11094](https://github.com/Microsoft/vscode-python/issues/11094)) + +### Fixes + +1. Ensure plot fits within the page of the `PDF`. + ([#9403](https://github.com/Microsoft/vscode-python/issues/9403)) +1. Fix typing in output of cells to not delete or modify any cells. + ([#9519](https://github.com/Microsoft/vscode-python/issues/9519)) +1. Show an error when ipywidgets cannot be found. + ([#9523](https://github.com/Microsoft/vscode-python/issues/9523)) +1. Experiments no longer block on telemetry. + ([#10008](https://github.com/Microsoft/vscode-python/issues/10008)) +1. Fix interactive window debugging after running cells in a notebook. + ([#10206](https://github.com/Microsoft/vscode-python/issues/10206)) +1. Fix problem with Data Viewer not working when builtin functions are overridden (like max). + ([#10280](https://github.com/Microsoft/vscode-python/issues/10280)) +1. Fix interactive window debugging when debugging the first cell to be run. + ([#10395](https://github.com/Microsoft/vscode-python/issues/10395)) +1. Fix interactive window debugging for extra lines in a function. + ([#10396](https://github.com/Microsoft/vscode-python/issues/10396)) +1. Notebook metadata is now initialized in the correct place. + ([#10544](https://github.com/Microsoft/vscode-python/issues/10544)) +1. Fix save button not working on notebooks. + ([#10647](https://github.com/Microsoft/vscode-python/issues/10647)) +1. Fix toolbars on 3rd party widgets to show correct icons. + ([#10734](https://github.com/Microsoft/vscode-python/issues/10734)) +1. Clicking or double clicking in output of a cell selects or gives focus to a cell. It should only affect the controls in the output. + ([#10749](https://github.com/Microsoft/vscode-python/issues/10749)) +1. Fix for notebooks not becoming dirty when changing a kernel. + ([#10795](https://github.com/Microsoft/vscode-python/issues/10795)) +1. Auto save for focusChange is not respected when switching to non text documents. Menu focus will still not cause a save (no callback from VS code for this), but should work for switching to other apps and non text documents. + ([#10853](https://github.com/Microsoft/vscode-python/issues/10853)) +1. Handle display.update inside of cells. + ([#10873](https://github.com/Microsoft/vscode-python/issues/10873)) +1. ZMQ should not cause local server to fail. + ([#10877](https://github.com/Microsoft/vscode-python/issues/10877)) +1. Fixes issue with spaces in debugger paths when using `getRemoteLauncherCommand`. + ([#10905](https://github.com/Microsoft/vscode-python/issues/10905)) +1. Fix output and interact widgets to work again. + ([#10915](https://github.com/Microsoft/vscode-python/issues/10915)) +1. Make sure the same python is used for the data viewer as the notebook so that pandas can be found. + ([#10926](https://github.com/Microsoft/vscode-python/issues/10926)) +1. Ensure user code in cell is preserved between cell execution and cell edits. + ([#10949](https://github.com/Microsoft/vscode-python/issues/10949)) +1. Make sure the interpreter in the notebook matches the kernel. + ([#10953](https://github.com/Microsoft/vscode-python/issues/10953)) +1. Jupyter notebooks and interactive window crashing on startup. + ([#11035](https://github.com/Microsoft/vscode-python/issues/11035)) +1. Fix perf problems after running the interactive window for an extended period of time. + ([#10971](https://github.com/Microsoft/vscode-python/issues/10971)) +1. Fix problem with opening a notebook in jupyter after saving in VS code. + ([#11151](https://github.com/Microsoft/vscode-python/issues/11151)) +1. Fix CTRL+Z and Z for undo on notebooks. + ([#11160](https://github.com/Microsoft/vscode-python/issues/11160)) +1. Fix saving to PDF for viewed plots. + ([#11157](https://github.com/Microsoft/vscode-python/issues/11157)) +1. Fix scrolling in a notebook whenever resizing or opening. + ([#11238](https://github.com/Microsoft/vscode-python/issues/11238)) + +### Code Health + +1. Add conda environments to nightly test runs. + ([#10134](https://github.com/Microsoft/vscode-python/issues/10134)) +1. Refactor the extension activation code to split on phases. + ([#10454](https://github.com/Microsoft/vscode-python/issues/10454)) +1. Added a kernel launcher to spawn python kernels without Jupyter. + ([#10479](https://github.com/Microsoft/vscode-python/issues/10479)) +1. Add ZMQ library to extension. + ([#10483](https://github.com/Microsoft/vscode-python/issues/10483)) +1. Added test harness for `ipywidgets` in `notebooks`. + ([#10655](https://github.com/Microsoft/vscode-python/issues/10655)) +1. Run internal modules and scripts in isolated manner. + This helps avoid problems like shadowing stdlib modules. + ([#10681](https://github.com/Microsoft/vscode-python/issues/10681)) +1. Add telemetry for .env files. + ([#10780](https://github.com/Microsoft/vscode-python/issues/10780)) +1. Update prettier to latest version. + ([#10837](https://github.com/Microsoft/vscode-python/issues/10837)) +1. Update typescript to `3.8`. + ([#10839](https://github.com/Microsoft/vscode-python/issues/10839)) +1. Add telemetry around ipywidgets usage, failures, and overhead. + ([#11027](https://github.com/Microsoft/vscode-python/issues/11027)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.3.2 (2 April 2020) + +### Fixes + +1. Update `debugpy` to latest (v1.0.0b5). Fixes issue with connections with multi-process. + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.3.1 (31 March 2020) + +### Fixes + +1. Update `debugpy` to latest (v1.0.0b4). Fixes issue with locale. + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.3.0 (19 March 2020) + +### Enhancements + +1. Make interactive window wrap like the notebook editor does. + ([#4466](https://github.com/Microsoft/vscode-python/issues/4466)) +1. Support scrolling beyond the last line in the notebook editor and the interactive window. Uses the `editor.scrollBeyondLastLine` setting. + ([#7892](https://github.com/Microsoft/vscode-python/issues/7892)) +1. Allow user to override the arguments passed to Jupyter on startup. To change the arguments, run the 'Python: Specify Jupyter command line arguments" command. + ([#8698](https://github.com/Microsoft/vscode-python/issues/8698)) +1. When entering remote Jupyter Server, default the input value to uri in clipboard. + ([#9163](https://github.com/Microsoft/vscode-python/issues/9163)) +1. Added a command to allow users to select a kernel for a `Notebook`. + ([#9228](https://github.com/Microsoft/vscode-python/issues/9228)) +1. When saving new `notebooks`, default to the current workspace folder. + ([#9331](https://github.com/Microsoft/vscode-python/issues/9331)) +1. When the output of a cell gets trimmed for the first time, the user will be informed of it and which setting changes it. + ([#9401](https://github.com/Microsoft/vscode-python/issues/9401)) +1. Change the parameters for when a Data Science survey prompt comes up. After opening 5 notebooks (ever) or running 100 cells (ever). + ([#10186](https://github.com/Microsoft/vscode-python/issues/10186)) +1. Show quickfixes for launch.json. + ([#10245](https://github.com/Microsoft/vscode-python/issues/10245)) + +### Fixes + +1. Jupyter autocompletion will only show magic commands on empty lines, preventing them of appearing in functions. + ([#10023](https://github.com/Microsoft/vscode-python/issues/10023)) +1. Remove extra lines at the end of the file when formatting with Black. + ([#1877](https://github.com/Microsoft/vscode-python/issues/1877)) +1. Capitalize `Activate.ps1` in code for PowerShell Core on Linux. + ([#2607](https://github.com/Microsoft/vscode-python/issues/2607)) +1. Change interactive window to use the python interpreter associated with the file being run. + ([#3123](https://github.com/Microsoft/vscode-python/issues/3123)) +1. Make line numbers in errors for the Interactive window match the original file and make them clickable for jumping back to an error location. + ([#6370](https://github.com/Microsoft/vscode-python/issues/6370)) +1. Fix magic commands that return 'paged' output. + ([#6900](https://github.com/Microsoft/vscode-python/issues/6900)) +1. Ensure model is updated with user changes after user types into the editor. + ([#8589](https://github.com/Microsoft/vscode-python/issues/8589)) +1. Fix latex output from a code cell to render correctly. + ([#8742](https://github.com/Microsoft/vscode-python/issues/8742)) +1. Toggling cell type from `code` to `markdown` will not set focus to the editor in cells of a `Notebook`. + ([#9102](https://github.com/Microsoft/vscode-python/issues/9102)) +1. Remove whitespace from code before pushing to the interactive window. + ([#9116](https://github.com/Microsoft/vscode-python/issues/9116)) +1. Have sys info show that we have connected to an existing server. + ([#9132](https://github.com/Microsoft/vscode-python/issues/9132)) +1. Fix IPython.clear_output to behave like Jupyter. + ([#9174](https://github.com/Microsoft/vscode-python/issues/9174)) +1. Jupyter output tab was not showing anything when connecting to a remote server. + ([#9177](https://github.com/Microsoft/vscode-python/issues/9177)) +1. Fixed our css generation from custom color themes which caused the Data Viewer to not load. + ([#9242](https://github.com/Microsoft/vscode-python/issues/9242)) +1. Allow a user to skip switching to a kernel if the kernel dies during startup. + ([#9250](https://github.com/Microsoft/vscode-python/issues/9250)) +1. Clean up interative window styling and set focus to input box if clicking in the interactive window. + ([#9282](https://github.com/Microsoft/vscode-python/issues/9282)) +1. Change icon spacing to match vscode icon spacing in native editor toolbars and interactive window toolbar. + ([#9283](https://github.com/Microsoft/vscode-python/issues/9283)) +1. Display diff viewer for `ipynb` files without opening `Notebooks`. + ([#9395](https://github.com/Microsoft/vscode-python/issues/9395)) +1. Python environments will not be activated in terminals hidden from the user. + ([#9503](https://github.com/Microsoft/vscode-python/issues/9503)) +1. Disable `Restart Kernel` and `Interrupt Kernel` buttons when a `kernel` has not yet started. + ([#9731](https://github.com/Microsoft/vscode-python/issues/9731)) +1. Fixed an issue with multiple latex formulas in the same '\$\$' block. + ([#9766](https://github.com/Microsoft/vscode-python/issues/9766)) +1. Make notebook editor and interactive window honor undocumented editor.scrollbar.verticalScrollbarSize option + increase default to match vscode. + ([#9803](https://github.com/Microsoft/vscode-python/issues/9803)) +1. Ensure that invalid kernels don't hang notebook startup or running. + ([#9845](https://github.com/Microsoft/vscode-python/issues/9845)) +1. Switching kernels should disable the run/interrupt/restart buttons. + ([#9935](https://github.com/Microsoft/vscode-python/issues/9935)) +1. Prompt to install `pandas` if not found when opening the `Data Viewer`. + ([#9944](https://github.com/Microsoft/vscode-python/issues/9944)) +1. Prompt to reload VS Code when changing the Jupyter Server connection. + ([#9945](https://github.com/Microsoft/vscode-python/issues/9945)) +1. Support opening spark dataframes in the data viewer. + ([#9959](https://github.com/Microsoft/vscode-python/issues/9959)) +1. Make sure metadata in a cell survives execution. + ([#9997](https://github.com/Microsoft/vscode-python/issues/9997)) +1. Fix run all cells to force each cell to finish before running the next one. + ([#10016](https://github.com/Microsoft/vscode-python/issues/10016)) +1. Fix interrupts from always thinking a restart occurred. + ([#10050](https://github.com/Microsoft/vscode-python/issues/10050)) +1. Do not delay activation of extension by waiting for terminal to get activated. + ([#10094](https://github.com/Microsoft/vscode-python/issues/10094)) +1. LiveShare can prevent the jupyter server from starting if it crashes. + ([#10097](https://github.com/Microsoft/vscode-python/issues/10097)) +1. Mark `poetry.lock` file as toml syntax. + (thanks to [remcohaszing](https://github.com/remcohaszing/)) + ([#10111](https://github.com/Microsoft/vscode-python/issues/10111)) +1. Hide input in `Interactive Window` based on the setting `allowInput`. + ([#10124](https://github.com/Microsoft/vscode-python/issues/10124)) +1. Fix scrolling for output to consistently scroll even during execution. + ([#10137](https://github.com/Microsoft/vscode-python/issues/10137)) +1. Correct image backgrounds for notebook editor. + ([#10154](https://github.com/Microsoft/vscode-python/issues/10154)) +1. Fix empty variables to show an empty string in the Notebook/Interactive Window variable explorer. + ([#10204](https://github.com/Microsoft/vscode-python/issues/10204)) +1. In addition to updating current working directory also add on our notebook file path to sys.path to match Jupyter. + ([#10227](https://github.com/Microsoft/vscode-python/issues/10227)) +1. Ensure message (about trimmed output) displayed in an output cell looks like a link. + ([#10231](https://github.com/Microsoft/vscode-python/issues/10231)) +1. Users can opt into or opt out of experiments in remote scenarios. + ([#10232](https://github.com/Microsoft/vscode-python/issues/10232)) +1. Ensure to correctly return env variables of the activated interpreter, when dealing with non-workspace interpreters. + ([#10250](https://github.com/Microsoft/vscode-python/issues/10250)) +1. Update kernel environments before each run to use the latest environment. Only do this for kernel specs created by the python extension. + ([#10255](https://github.com/Microsoft/vscode-python/issues/10255)) +1. Don't start up and shutdown an extra Jupyter notebook on server startup. + ([#10311](https://github.com/Microsoft/vscode-python/issues/10311)) +1. When you install missing dependencies for Jupyter successfully in an active interpreter also set that interpreter as the Jupyter selected interpreter. + ([#10359](https://github.com/Microsoft/vscode-python/issues/10359)) +1. Ensure default `host` is not set, if `connect` or `listen` settings are available. + ([#10597](https://github.com/Microsoft/vscode-python/issues/10597)) + +### Code Health + +1. Use the new VS Code filesystem API as much as possible. + ([#6911](https://github.com/Microsoft/vscode-python/issues/6911)) +1. Functional tests using real jupyter can take 30-90 seconds each. Most of this time is searching for interpreters. Cache the interpreter search. + ([#7997](https://github.com/Microsoft/vscode-python/issues/7997)) +1. Use Python 3.8 in tests run on Azure DevOps. + ([#8298](https://github.com/Microsoft/vscode-python/issues/8298)) +1. Display `Commands` related to `Interactive Window` and `Notebooks` only when necessary. + ([#8869](https://github.com/Microsoft/vscode-python/issues/8869)) +1. Change cursor styles of buttons `pointer` in `Interactive Window` and `Native Editor`. + ([#9341](https://github.com/Microsoft/vscode-python/issues/9341)) +1. Update Jedi to 0.16.0. + ([#9765](https://github.com/Microsoft/vscode-python/issues/9765)) +1. Update version of `VSCode` in `package.json` to `1.42`. + ([#10046](https://github.com/Microsoft/vscode-python/issues/10046)) +1. Capture `mimetypes` of cell outputs. + ([#10182](https://github.com/Microsoft/vscode-python/issues/10182)) +1. Use debugpy in the core extension instead of ptvsd. + ([#10184](https://github.com/Microsoft/vscode-python/issues/10184)) +1. Add telemetry for imports in notebooks. + ([#10209](https://github.com/Microsoft/vscode-python/issues/10209)) +1. Update data science component to use `debugpy`. + ([#10211](https://github.com/Microsoft/vscode-python/issues/10211)) +1. Use new MacOS VM in Pipelines. + ([#10288](https://github.com/Microsoft/vscode-python/issues/10288)) +1. Split the windows PR tests into two sections so they do not time out. + ([#10293](https://github.com/Microsoft/vscode-python/issues/10293)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [debugpy](https://pypi.org/project/debugpy/) +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.2.3 (21 February 2020) + +### Fixes + +1. Ensure to correctly return env variables of the activated interpreter, when dealing with non-workspace interpreters. + ([#10250](https://github.com/Microsoft/vscode-python/issues/10250)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.2.2 (19 February 2020) + +### Fixes + +1. Improve error messaging when the jupyter notebook cannot be started. + ([#9904](https://github.com/Microsoft/vscode-python/issues/9904)) +1. Clear variables in notebooks and interactive-window when restarting. + ([#9991](https://github.com/Microsoft/vscode-python/issues/9991)) +1. Re-install `Jupyter` instead of installing `kernelspec` if `kernelspec` cannot be found in the python environment. + ([#10071](https://github.com/Microsoft/vscode-python/issues/10071)) +1. Fixes problem with showing ndarrays in the data viewer. + ([#10074](https://github.com/Microsoft/vscode-python/issues/10074)) +1. Fix data viewer not opening on certain data frames. + ([#10075](https://github.com/Microsoft/vscode-python/issues/10075)) +1. Fix svg mimetype so it shows up correctly in richest mimetype order. + ([#10168](https://github.com/Microsoft/vscode-python/issues/10168)) +1. Perf improvements to executing startup code for `Data Science` features when extension loads. + ([#10170](https://github.com/Microsoft/vscode-python/issues/10170)) + +### Code Health + +1. Add telemetry to track notebook languages + ([#9819](https://github.com/Microsoft/vscode-python/issues/9819)) +1. Telemetry around kernels not working and installs not working. + ([#9883](https://github.com/Microsoft/vscode-python/issues/9883)) +1. Change select kernel telemetry to track duration till quick pick appears. + ([#10049](https://github.com/Microsoft/vscode-python/issues/10049)) +1. Track cold/warm times to execute notebook cells. + ([#10176](https://github.com/Microsoft/vscode-python/issues/10176)) +1. Telemetry to capture connections to `localhost` using the connect to remote Jupyter server feature. + ([#10098](https://github.com/Microsoft/vscode-python/issues/10098)) +1. Telemetry to capture perceived startup times of Jupyter and time to execute a cell. + ([#10212](https://github.com/Microsoft/vscode-python/issues/10212)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.2.1 (12 February 2020) + +### Fixes + +1. Re-install `Jupyter` instead of installing `kernelspec` if `kernelspec` cannot be found in the python environment. + ([#10071](https://github.com/Microsoft/vscode-python/issues/10071)) +1. Fix zh-tw localization file loading issue. + (thanks to [ChenKB91](https://github.com/ChenKB91/)) + ([#10072](https://github.com/Microsoft/vscode-python/issues/10072)) + +### Note + +1. Please only set the `python.languageServer` setting if you want to turn IntelliSense off. To switch between language servers, please keep using the `python.jediEnabled` setting for now. + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.2.0 (11 February 2020) + +### Enhancements + +1. Support opting in and out of an experiment group. + ([#6816](https://github.com/Microsoft/vscode-python/issues/6816)) +1. Add `python.languageServer` setting with values of `Jedi` (acts same as `jediEnabled`), + `Microsoft` for the Microsoft Python Language Server and `None`, which suppresses + editor support in the extension so neither Jedi nor Microsoft Python Language Server + start. `None` is useful for those users who prefer using other extensions for the + editor functionality. + ([#7010](https://github.com/Microsoft/vscode-python/issues/7010)) +1. Automatically start the Jupyter server when opening a notebook or the interative window, or when either of those has happened in the last 7 days. This behavior can be disabled with the 'python.dataScience.disableJupyterAutoStart' setting. + ([#7232](https://github.com/Microsoft/vscode-python/issues/7232)) +1. Add support for rendering local images within markdown cells in the `Notebook Editor`. + ([#7704](https://github.com/Microsoft/vscode-python/issues/7704)) +1. Add progress indicator for starting of jupyter with details of each stage. + ([#7868](https://github.com/Microsoft/vscode-python/issues/7868)) +1. Use a dedicated Python Interpreter for starting `Jupyter Notebook Server`. + This can be changed using the command `Select Interpreter to start Jupyter server` from the `Command Palette`. + ([#8623](https://github.com/Microsoft/vscode-python/issues/8623)) +1. Implement pid quick pick for attach cases with the new debug adapter. + ([#8701](https://github.com/Microsoft/vscode-python/issues/8701)) +1. Provide attach to pid configuration via picker. + ([#8702](https://github.com/Microsoft/vscode-python/issues/8702)) +1. Support for custom python debug adapter. + ([#8720](https://github.com/Microsoft/vscode-python/issues/8720)) +1. Remove insiders re-enroll prompt. + ([#8775](https://github.com/Microsoft/vscode-python/issues/8775)) +1. Attach to pid picker - bodyblock users who are not in the new debugger experiment. + ([#8935](https://github.com/Microsoft/vscode-python/issues/8935)) +1. Pass `-y` to `conda installer` to disable the prompt to install, as user has already ok'ed this action. + ([#9194](https://github.com/Microsoft/vscode-python/issues/9194)) +1. Updated `ptvsd` debugger to version v5.0.0a12. + ([#9310](https://github.com/Microsoft/vscode-python/issues/9310)) +1. Use common code to manipulate notebook cells. + ([#9386](https://github.com/Microsoft/vscode-python/issues/9386)) +1. Add support for `Find` in the `Notebook Editor`. + ([#9470](https://github.com/Microsoft/vscode-python/issues/9470)) +1. Update Chinese (Traditional) translation. + (thanks [pan93412](https://github.com/pan93412)) + ([#9548](https://github.com/Microsoft/vscode-python/issues/9548)) +1. Look for Conda interpreters in `~/opt/*conda*/` directory as well. + ([#9701](https://github.com/Microsoft/vscode-python/issues/9701)) + +### Fixes + +1. add --ip=127.0.0.1 argument of jupyter server when running in k8s container + ([#9976](https://github.com/Microsoft/vscode-python/issues/9976)) +1. Correct the server and kernel text for when not connected to a server. + ([#9933](https://github.com/Microsoft/vscode-python/issues/9933)) +1. Make sure to clear variable list on restart kernel. + ([#9740](https://github.com/Microsoft/vscode-python/issues/9740)) +1. Use the autoStart server when available. + ([#9926](https://github.com/Microsoft/vscode-python/issues/9926)) +1. Removed unnecessary warning when executing cells that use Scrapbook, + Fix an html crash when using not supported mime types + ([#9796](https://github.com/microsoft/vscode-python/issues/9796)) +1. Fixed the focus on the interactive window when pressing ctrl + 1/ ctrl + 2 + ([#9693](https://github.com/microsoft/vscode-python/issues/9693)) +1. Fix variable explorer in Interactive and Notebook editors from interfering with execution. + ([#5980](https://github.com/Microsoft/vscode-python/issues/5980)) +1. Fix a crash when using pytest to discover doctests with unknown line number. + (thanks [Olivier Grisel](https://github.com/ogrisel/)) + ([#7487](https://github.com/Microsoft/vscode-python/issues/7487)) +1. Don't show any install product prompts if interpreter is not selected. + ([#7750](https://github.com/Microsoft/vscode-python/issues/7750)) +1. Allow PYTHONWARNINGS to be set and not have it interfere with the launching of Jupyter notebooks. + ([#8496](https://github.com/Microsoft/vscode-python/issues/8496)) +1. Pressing Esc in the config quickpick now cancels debugging. + ([#8626](https://github.com/Microsoft/vscode-python/issues/8626)) +1. Support resolveCompletionItem so that we can get Jedi docstrings in Notebook Editor and Interactive Window. + ([#8706](https://github.com/Microsoft/vscode-python/issues/8706)) +1. Disable interrupt, export, and restart buttons when already performing an interrupt, export, or restart for Notebooks and the Interactive window. + ([#8716](https://github.com/Microsoft/vscode-python/issues/8716)) +1. Icons now cannot be overwritten by styles in cell outputs. + ([#8946](https://github.com/Microsoft/vscode-python/issues/8946)) +1. Command palette (and other keyboard shortcuts) don't work from the Interactive/Notebook editor in the insider's build (or when setting 'useWebViewServer'). + ([#8976](https://github.com/Microsoft/vscode-python/issues/8976)) +1. Fix issue that prevented language server diagnostics from being published. + ([#9096](https://github.com/Microsoft/vscode-python/issues/9096)) +1. Fixed the native editor toolbar so it won't overlap. + ([#9140](https://github.com/Microsoft/vscode-python/issues/9140)) +1. Selectively render output and monaco editor to improve performance. + ([#9204](https://github.com/Microsoft/vscode-python/issues/9204)) +1. Set test debug console default to be `internalConsole`. + ([#9259](https://github.com/Microsoft/vscode-python/issues/9259)) +1. Fix the Data Science "Enable Plot Viewer" setting to pass figure_formats correctly when turned off. + ([#9420](https://github.com/Microsoft/vscode-python/issues/9420)) +1. Shift+Enter can no longer send multiple lines to the interactive window. + ([#9437](https://github.com/Microsoft/vscode-python/issues/9437)) +1. Shift+Enter can no longer run code in the terminal. + ([#9439](https://github.com/Microsoft/vscode-python/issues/9439)) +1. Scrape output to get the details of the registered kernel. + ([#9444](https://github.com/Microsoft/vscode-python/issues/9444)) +1. Update `ptvsd` debugger to version v5.0.0a11. Fixes signing for `inject_dll_x86.exe`. + ([#9474](https://github.com/Microsoft/vscode-python/issues/9474)) +1. Disable use of `conda run`. + ([#9490](https://github.com/Microsoft/vscode-python/issues/9490)) +1. Improvements to responsiveness of code completions in `Notebook` cells and `Interactive Window`. + ([#9494](https://github.com/Microsoft/vscode-python/issues/9494)) +1. Revert changes related to calling `mypy` with relative paths. + ([#9496](https://github.com/Microsoft/vscode-python/issues/9496)) +1. Remove default `pathMappings` for attach to local process by process Id. + ([#9533](https://github.com/Microsoft/vscode-python/issues/9533)) +1. Ensure event handler is bound to the right context. + ([#9539](https://github.com/Microsoft/vscode-python/issues/9539)) +1. Use the correct interpreter when creating the Python execution service used as a fallback by the Daemon. + ([#9566](https://github.com/Microsoft/vscode-python/issues/9566)) +1. Ensure environment variables are always strings in `launch.json`. + ([#9568](https://github.com/Microsoft/vscode-python/issues/9568)) +1. Fix error in developer console about serializing gather rules. + ([#9571](https://github.com/Microsoft/vscode-python/issues/9571)) +1. Do not open the output panel when building workspace symbols. + ([#9603](https://github.com/Microsoft/vscode-python/issues/9603)) +1. Use an activated environment python process to check if modules are installed. + ([#9643](https://github.com/Microsoft/vscode-python/issues/9643)) +1. When hidden 'useWebViewServer' is true, clicking on links in Notebook output don't work. + ([#9645](https://github.com/Microsoft/vscode-python/issues/9645)) +1. Always use latest version of the debugger when building extension. + ([#9652](https://github.com/Microsoft/vscode-python/issues/9652)) +1. Fix background for interactive window copy icon. + ([#9658](https://github.com/Microsoft/vscode-python/issues/9658)) +1. Fix text in markdown cells being lost when clicking away. + ([#9719](https://github.com/Microsoft/vscode-python/issues/9719)) +1. Fix debugging of Interactive Window cells. Don't start up a second notebook at Interactive Window startup. + ([#9780](https://github.com/Microsoft/vscode-python/issues/9780)) +1. When comitting intellisense in Notebook Editor with Jedi place code in correct position. + ([#9857](https://github.com/Microsoft/vscode-python/issues/9857)) +1. Ignore errors coming from stat(), where appropriate. + ([#9901](https://github.com/Microsoft/vscode-python/issues/9901)) + +### Code Health + +1. Use [prettier](https://prettier.io/) as the `TypeScript` formatter and [Black](https://github.com/psf/black) as the `Python` formatter within the extension. + ([#2012](https://github.com/Microsoft/vscode-python/issues/2012)) +1. Use `vanillajs` for build scripts (instead of `typescript`, avoids the step of having to transpile). + ([#5674](https://github.com/Microsoft/vscode-python/issues/5674)) +1. Remove npx from webpack build as it [breaks on windows](https://github.com/npm/npx/issues/5) on npm 6.11+ and doesn't seem to be getting fixes. Update npm to current version. + ([#7197](https://github.com/Microsoft/vscode-python/issues/7197)) +1. Clean up npm dependencies. + ([#8302](https://github.com/Microsoft/vscode-python/issues/8302)) +1. Update version of node to `12.4.0`. + ([#8453](https://github.com/Microsoft/vscode-python/issues/8453)) +1. Use a hidden terminal to retrieve environment variables of an activated Python Interpreter. + ([#8928](https://github.com/Microsoft/vscode-python/issues/8928)) +1. Fix broken LiveShare connect via codewatcher test. + ([#9005](https://github.com/Microsoft/vscode-python/issues/9005)) +1. Refactor `webpack` build scripts to build `DS` bundles using separate config files. + ([#9055](https://github.com/Microsoft/vscode-python/issues/9055)) +1. Change how we handle keyboard input for our functional editor tests. + ([#9084](https://github.com/Microsoft/vscode-python/issues/9084)) +1. Fix working directory path verification for notebook tests. + ([#9191](https://github.com/Microsoft/vscode-python/issues/9191)) +1. Update Jedi to 0.15.2 and parso to 0.5.2. + ([#9243](https://github.com/Microsoft/vscode-python/issues/9243)) +1. Added a test performance measuring pipeline. + ([#9421](https://github.com/Microsoft/vscode-python/issues/9421)) +1. Audit existing telemetry events for datascience or ds_internal. + ([#9626](https://github.com/Microsoft/vscode-python/issues/9626)) +1. CI failure on Data science memoize-one dependency being removed. + ([#9646](https://github.com/Microsoft/vscode-python/issues/9646)) +1. Make sure to check dependencies during PRs. + ([#9714](https://github.com/Microsoft/vscode-python/issues/9714)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2020.1.0 (6 January 2020) + +### Enhancements + +1. Added experiment for reloading feature of debugging web apps. + ([#3473](https://github.com/Microsoft/vscode-python/issues/3473)) +1. Activate conda environment using path when name is not available. + ([#3834](https://github.com/Microsoft/vscode-python/issues/3834)) +1. Add QuickPick dropdown option _Run All/Debug All_ when clicking on a Code Lens for a parametrized test to be able to run/debug all belonging test variants at once. + (thanks to [Philipp Loose](https://github.com/phloose)) + ([#5608](https://github.com/Microsoft/vscode-python/issues/5608)) +1. Use Octicons in Code Lenses. (thanks [Aidan Dang](https://github.com/AidanGG)) + ([#7192](https://github.com/Microsoft/vscode-python/issues/7192)) +1. Improve startup performance of Jupyter by using a Python daemon. + ([#7242](https://github.com/Microsoft/vscode-python/issues/7242)) +1. Automatically indent following `async for` and `async with` statements. + ([#7344](https://github.com/Microsoft/vscode-python/issues/7344)) +1. Added extension option `activateEnvInCurrentTerminal` to detect if environment should be activated in the current open terminal. + ([#7665](https://github.com/Microsoft/vscode-python/issues/7665)) +1. Add telemetry for usage of activateEnvInCurrentTerminal setting. + ([#8004](https://github.com/Microsoft/vscode-python/issues/8004)) +1. Support multiprocess debugging using the new python debug adapter. + ([#8105](https://github.com/Microsoft/vscode-python/issues/8105)) +1. Support a per interpreter language server so that notebooks that aren't using the currently selected python can still have intellisense. + ([#8206](https://github.com/Microsoft/vscode-python/issues/8206)) +1. Add "processId" key in launch.json to enable attach-to-local-pid scenarios when using the new debug adapter. + ([#8384](https://github.com/Microsoft/vscode-python/issues/8384)) +1. Populate survey links with variables + ([#8484](https://github.com/Microsoft/vscode-python/issues/8484)) +1. Support the ability to take input from users inside of a notebook or the Interactive Window. + ([#8601](https://github.com/Microsoft/vscode-python/issues/8601)) +1. Create an MRU list for Jupyter notebook servers. + ([#8613](https://github.com/Microsoft/vscode-python/issues/8613)) +1. Add icons to the quick pick list for specifying the Jupyter server URI. + ([#8753](https://github.com/Microsoft/vscode-python/issues/8753)) +1. Added kernel status and selection toolbar to the notebook editor. + ([#8866](https://github.com/Microsoft/vscode-python/issues/8866)) +1. Updated `ptvsd` debugger to version v5.0.0a9. + ([#8930](https://github.com/Microsoft/vscode-python/issues/8930)) +1. Add ability to select an existing remote `kernel`. + ([#4644](https://github.com/Microsoft/vscode-python/issues/4644)) +1. Notify user when starting jupyter times out and added `Jupyter` output panel to display output from Jupyter. + ([#9068](https://github.com/Microsoft/vscode-python/issues/9068)) + +### Fixes + +1. Add implementations for `python.workspaceSymbols.rebuildOnStart` and `python.workspaceSymbols.rebuildOnFileSave`. + ([#793](https://github.com/Microsoft/vscode-python/issues/793)) +1. Use relative paths when invoking mypy. + (thanks to [yxliang01](https://github.com/yxliang01)) + ([#5326](https://github.com/Microsoft/vscode-python/issues/5326)) +1. Make the dataviewer open a window much faster. Total load time is the same, but initial response is much faster. + ([#6729](https://github.com/Microsoft/vscode-python/issues/6729)) +1. Make sure the data viewer for notebooks comes up as soon as the user clicks. + ([#6840](https://github.com/Microsoft/vscode-python/issues/6840)) +1. Support saving plotly graphs in the Interactive Window or inside of a notebook. + ([#7221](https://github.com/Microsoft/vscode-python/issues/7221)) +1. Change 0th line in output to 1th in flake8. + (thanks to [Ma007ks](https://github.com/Ma007ks/)) + ([#7349](https://github.com/Microsoft/vscode-python/issues/7349)) +1. Support local images in markdown and output for notebooks. + ([#7704](https://github.com/Microsoft/vscode-python/issues/7704)) +1. Default notebookFileRoot to match the file that a notebook was opened with (or the first file run for the interactive window). + ([#7780](https://github.com/Microsoft/vscode-python/issues/7780)) +1. Execution count and output are cleared from the .ipynb file when the user clicks the 'Clear All Output'. + ([#7853](https://github.com/Microsoft/vscode-python/issues/7853)) +1. Fix clear_output(True) to work in notebook cells. + ([#7970](https://github.com/Microsoft/vscode-python/issues/7970)) +1. Prevented '\$0' from appearing inside brackets when using intellisense autocomplete. + ([#8101](https://github.com/Microsoft/vscode-python/issues/8101)) +1. Intellisense can sometimes not appear in notebooks or the interactive window, especially when something is a large list. + ([#8140](https://github.com/Microsoft/vscode-python/issues/8140)) +1. Correctly update interpreter and kernel info in the metadata. + ([#8223](https://github.com/Microsoft/vscode-python/issues/8223)) +1. Dataframe viewer should use the same interpreter as the active notebook is using. + ([#8227](https://github.com/Microsoft/vscode-python/issues/8227)) +1. 'breakpoint' line shows up in the interactive window when debugging a cell. + ([#8260](https://github.com/Microsoft/vscode-python/issues/8260)) +1. Run above should include all code, and not just cells above. + ([#8403](https://github.com/Microsoft/vscode-python/issues/8403)) +1. Fix issue with test discovery when using `unittest` with `--pattern` flag. + ([#8465](https://github.com/Microsoft/vscode-python/issues/8465)) +1. Set focus to the corresponding `Native Notebook Editor` when opening an `ipynb` file again. + ([#8506](https://github.com/Microsoft/vscode-python/issues/8506)) +1. Fix using all environment variables when running in integrated terminal. + ([#8584](https://github.com/Microsoft/vscode-python/issues/8584)) +1. Fix display of SVG images from previously executed ipynb files. + ([#8600](https://github.com/Microsoft/vscode-python/issues/8600)) +1. Fixes that the test selection drop-down did not open when a code lens for a parameterized test was clicked on windows. + ([#8627](https://github.com/Microsoft/vscode-python/issues/8627)) +1. Changes to how `node-fetch` is bundled in the extension. + ([#8665](https://github.com/Microsoft/vscode-python/issues/8665)) +1. Re-enable support for source-maps. + ([#8686](https://github.com/Microsoft/vscode-python/issues/8686)) +1. Fix order for print/display outputs in a notebook cell. + ([#8739](https://github.com/Microsoft/vscode-python/issues/8739)) +1. Fix scrolling inside of intellisense hover windows for notebooks. + ([#8843](https://github.com/Microsoft/vscode-python/issues/8843)) +1. Fix scrolling in large cells. + ([#8895](https://github.com/Microsoft/vscode-python/issues/8895)) +1. Set `python.workspaceSymbols.enabled` to false by default. + ([#9046](https://github.com/Microsoft/vscode-python/issues/9046)) +1. Add ability to pick a remote kernel. + ([#3763](https://github.com/Microsoft/vscode-python/issues/3763)) +1. Do not set "redirectOutput": true by default when not specified in launch.json, unless "console" is "internalConsole". + ([#8865](https://github.com/Microsoft/vscode-python/issues/8865)) +1. Fix slowdown in Notebook editor caused by using global storage for too much data. + ([#8961](https://github.com/Microsoft/vscode-python/issues/8961)) +1. 'y' and 'm' keys toggle cell type but also add a 'y' or 'm' to the cell. + ([#9078](https://github.com/Microsoft/vscode-python/issues/9078)) +1. Remove unnecessary matplotlib import from first cell. + ([#9099](https://github.com/Microsoft/vscode-python/issues/9099)) +1. Two 'default' options in the select a Jupyter server URI picker. + ([#9101](https://github.com/Microsoft/vscode-python/issues/9101)) +1. Plot viewer never opens. + ([#9114](https://github.com/Microsoft/vscode-python/issues/9114)) +1. Fix color contrast for kernel selection control. + ([#9138](https://github.com/Microsoft/vscode-python/issues/9138)) +1. Disconnect between displayed server and connected server in Kernel selection UI. + ([#9151](https://github.com/Microsoft/vscode-python/issues/9151)) +1. Eliminate extra storage space from global storage on first open of a notebook that had already written to storage. + ([#9159](https://github.com/Microsoft/vscode-python/issues/9159)) +1. Change kernel selection MRU to just save connection time and don't try to connect when popping the list. Plus add unit tests for it. + ([#9171](https://github.com/Microsoft/vscode-python/issues/9171)) + +### Code Health + +1. Re-enable our mac 3.7 debugger tests as a blocking ptvsd issue has been resolved. + ([#6646](https://github.com/Microsoft/vscode-python/issues/6646)) +1. Use "conda run" (instead of using the "python.pythonPath" setting directly) when executing + Python and an Anaconda environment is selected. + ([#7696](https://github.com/Microsoft/vscode-python/issues/7696)) +1. Change state management for react code to use redux. + ([#7949](https://github.com/Microsoft/vscode-python/issues/7949)) +1. Pass resource when accessing VS Code settings. + ([#8001](https://github.com/Microsoft/vscode-python/issues/8001)) +1. Adjust some notebook and interactive window telemetry. + ([#8254](https://github.com/Microsoft/vscode-python/issues/8254)) +1. Added a new telemetry event called `DATASCIENCE.NATIVE.OPEN_NOTEBOOK_ALL` that fires every time the user opens a jupyter notebook by any means. + ([#8262](https://github.com/Microsoft/vscode-python/issues/8262)) +1. Create python daemon for execution of python code. + ([#8451](https://github.com/Microsoft/vscode-python/issues/8451)) +1. Update npm package `https-proxy-agent` by updating the packages that pull it in. + ([#8537](https://github.com/Microsoft/vscode-python/issues/8537)) +1. Improve startup times of unit tests by optionally ignoring some bootstrapping required for `monaco` and `react` tests. + ([#8564](https://github.com/Microsoft/vscode-python/issues/8564)) +1. Skip checking dependencies on CI in PRs. + ([#8840](https://github.com/Microsoft/vscode-python/issues/8840)) +1. Fix installation of sqlite on CI linux machines. + ([#8883](https://github.com/Microsoft/vscode-python/issues/8883)) +1. Fix the "convert to python" functional test failure. + ([#8899](https://github.com/Microsoft/vscode-python/issues/8899)) +1. Remove unused auto-save-enabled telemetry. + ([#8906](https://github.com/Microsoft/vscode-python/issues/8906)) +1. Added ability to wait for completion of the installation of modules. + ([#8952](https://github.com/Microsoft/vscode-python/issues/8952)) +1. Fix failing Data Viewer functional tests. + ([#8992](https://github.com/Microsoft/vscode-python/issues/8992)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.11.1 (22 November 2019) + +### Fixes + +1. Some LaTeX equations do not print in notebooks or the interactive window. + ([#8673](https://github.com/Microsoft/vscode-python/issues/8673)) +1. Converting to python script no longer working from a notebook. + ([#8677](https://github.com/Microsoft/vscode-python/issues/8677)) +1. Fixes to starting `Jupyter` in a `Docker` container. + ([#8661](https://github.com/Microsoft/vscode-python/issues/8661)) +1. Ensure arguments are generated correctly for `getRemoteLauncherCommand` when in debugger experiment. + ([#8685](https://github.com/Microsoft/vscode-python/issues/8685)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.11.0 (18 November 2019) + +### Enhancements + +1. Add Vega support into our list of transforms for output. + ([#4125](https://github.com/Microsoft/vscode-python/issues/4125)) +1. Add `.flake8` file association as ini-file. + (thanks [thernstig](https://github.com/thernstig/)) + ([#6506](https://github.com/Microsoft/vscode-python/issues/6506)) +1. Provide user feedback when searching for a Jupyter server to use and allow the user to cancel this process. + ([#7262](https://github.com/Microsoft/vscode-python/issues/7262)) +1. By default, don't change matplotlib themes and place all plots on a white background regardless of VS Code theme. Add a setting to allow for plots to try to theme. + ([#8000](https://github.com/Microsoft/vscode-python/issues/8000)) +1. Prompt to open exported `Notebook` in the `Notebook Editor`. + ([#8078](https://github.com/Microsoft/vscode-python/issues/8078)) +1. Add commands translation for 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. + (thanks [Nikolay Kondratyev](https://github.com/kondratyev-nv/)) + ([#8103](https://github.com/Microsoft/vscode-python/issues/8103)) +1. Add logging support for python debug adapter. + ([#8106](https://github.com/Microsoft/vscode-python/issues/8106)) +1. Style adjustments to line numbers (color and width) in the `Native Editor`, to line up with VS Code styles. + ([#8289](https://github.com/Microsoft/vscode-python/issues/8289)) +1. Added command translations for Turkish. + (thanks to [alioguzhan](https://github.com/alioguzhan/)) + ([#8320](https://github.com/Microsoft/vscode-python/issues/8320)) +1. Toolbar was updated to take less space and be reached more easily. + ([#8366](https://github.com/Microsoft/vscode-python/issues/8366)) + +### Fixes + +1. Fix running a unittest file executing only the first test. + (thanks [Nikolay Kondratyev](https://github.com/kondratyev-nv/)) + ([#4567](https://github.com/Microsoft/vscode-python/issues/4567)) +1. Force the pytest root dir to always be the workspace root folder. + ([#6548](https://github.com/Microsoft/vscode-python/issues/6548)) +1. The notebook editor will now treat wrapped lines as different lines, so moving in cells and between cells with the arrow keys (and j and k) will be easier. + ([#7227](https://github.com/Microsoft/vscode-python/issues/7227)) +1. During test discovery, ignore tests generated by pytest plugins (like pep8). + Tests like that were causing discovery to fail. + ([#7287](https://github.com/Microsoft/vscode-python/issues/7287)) +1. When exporting a notebook editor to python script don't use the temp file location for generating the export. + ([#7567](https://github.com/Microsoft/vscode-python/issues/7567)) +1. Unicode symbol used to mark skipped tests was almost not visible on Linux and Windows. + ([#7705](https://github.com/Microsoft/vscode-python/issues/7705)) +1. Editing cells in a notebook, closing VS code, and then reopening will not have the cell content visible. + ([#7754](https://github.com/Microsoft/vscode-python/issues/7754)) +1. Sonar warnings. + ([#7812](https://github.com/Microsoft/vscode-python/issues/7812)) +1. Remove --ci flag from install_ptvsd.py to fix execution of "Setup" instructions from CONTRIBUTING.md. + ([#7814](https://github.com/Microsoft/vscode-python/issues/7814)) +1. Add telemetry for control groups in debug adapter experiments. + ([#7817](https://github.com/Microsoft/vscode-python/issues/7817)) +1. Allow the language server to pick a default caching mode. + ([#7821](https://github.com/Microsoft/vscode-python/issues/7821)) +1. Respect ignoreVSCodeTheme setting and correctly swap icons when changing from light to dark color themes. + ([#7847](https://github.com/Microsoft/vscode-python/issues/7847)) +1. 'Clear All Output' now deletes execution count for all cells. + ([#7853](https://github.com/Microsoft/vscode-python/issues/7853)) +1. If a Jupyter server fails to start, allow user to retry without having to restart VS code. + ([#7865](https://github.com/Microsoft/vscode-python/issues/7865)) +1. Fix strings of commas appearing in text/html output in the notebook editor. + ([#7873](https://github.com/Microsoft/vscode-python/issues/7873)) +1. When creating a new blank notebook, it has existing text in it already. + ([#7980](https://github.com/Microsoft/vscode-python/issues/7980)) +1. Can now include a LaTeX-style equation without surrounding the equation with '\$' in a markdown cell. + ([#7992](https://github.com/Microsoft/vscode-python/issues/7992)) +1. Make a spinner appear during executing a cell. + ([#8003](https://github.com/Microsoft/vscode-python/issues/8003)) +1. Signature help is overflowing out of the signature help widget on the Notebook Editor. + ([#8006](https://github.com/Microsoft/vscode-python/issues/8006)) +1. Ensure intellisense (& similar widgets/popups) are dispaled for one cell in the Notebook editor. + ([#8007](https://github.com/Microsoft/vscode-python/issues/8007)) +1. Correctly restart Jupyter sessions when the active interpreter is changed. + ([#8019](https://github.com/Microsoft/vscode-python/issues/8019)) +1. Clear up wording around jupyterServerURI and remove the quick pick from the flow of setting that. + ([#8021](https://github.com/Microsoft/vscode-python/issues/8021)) +1. Use actual filename comparison for filename equality checks. + ([#8022](https://github.com/Microsoft/vscode-python/issues/8022)) +1. Opening a notebook a second time round with changes (made from another editor) should be preserved. + ([#8025](https://github.com/Microsoft/vscode-python/issues/8025)) +1. Minimize the GPU impact of the interactive window and the notebook editor. + ([#8039](https://github.com/Microsoft/vscode-python/issues/8039)) +1. Store version of the `Python` interpreter (kernel) in the notebook metadata when running cells. + ([#8064](https://github.com/Microsoft/vscode-python/issues/8064)) +1. Make shift+enter not take focus unless about to add a new cell. + ([#8069](https://github.com/Microsoft/vscode-python/issues/8069)) +1. When checking the version of `pandas`, use the same interpreter used to start `Jupyter`. + ([#8084](https://github.com/Microsoft/vscode-python/issues/8084)) +1. Make brackets and paranthesis auto complete in the Notebook Editor and Interactive Window (based on editor settings). + ([#8086](https://github.com/Microsoft/vscode-python/issues/8086)) +1. Cannot create more than one blank notebook. + ([#8132](https://github.com/Microsoft/vscode-python/issues/8132)) +1. Fix for code disappearing after switching between markdown and code in a Notebook Editor. + ([#8141](https://github.com/Microsoft/vscode-python/issues/8141)) +1. Support `⌘+s` keyboard shortcut for saving `Notebooks`. + ([#8151](https://github.com/Microsoft/vscode-python/issues/8151)) +1. Fix closing a Notebook Editor to actually wait for the kernel to restart. + ([#8167](https://github.com/Microsoft/vscode-python/issues/8167)) +1. Inserting a cell in a notebook can sometimes cause the contents to be the cell below it. + ([#8194](https://github.com/Microsoft/vscode-python/issues/8194)) +1. Scroll the notebook editor when giving focus or changing line of a code cell. + ([#8205](https://github.com/Microsoft/vscode-python/issues/8205)) +1. Prevent code from changing in the Notebook Editor while running a cell. + ([#8215](https://github.com/Microsoft/vscode-python/issues/8215)) +1. When updating the Python extension, unsaved changes to notebooks are lost. + ([#8263](https://github.com/Microsoft/vscode-python/issues/8263)) +1. Fix CI to use Python 3.7.5. + ([#8296](https://github.com/Microsoft/vscode-python/issues/8296)) +1. Correctly transition markdown cells into code cells. + ([#8386](https://github.com/Microsoft/vscode-python/issues/8386)) +1. Fix cells being erased when saving and then changing focus to another cell. + ([#8399](https://github.com/Microsoft/vscode-python/issues/8399)) +1. Add a white background for most non-text mimetypes. This lets stuff like Atlair look good in dark mode. + ([#8423](https://github.com/Microsoft/vscode-python/issues/8423)) +1. Export to python button is blue in native editor. + ([#8424](https://github.com/Microsoft/vscode-python/issues/8424)) +1. CTRL+Z is deleting cells. It should only undo changes inside of the code for a cell. 'Z' and 'SHIFT+Z' are for undoing/redoing cell adds/moves. + ([#7999](https://github.com/Microsoft/vscode-python/issues/7999)) +1. Ensure clicking `ctrl+s` in a new `notebook` prompts the user to select a file once instead of twice. + ([#8138](https://github.com/Microsoft/vscode-python/issues/8138)) +1. Creating a new blank notebook should not require a search for jupyter. + ([#8481](https://github.com/Microsoft/vscode-python/issues/8481)) +1. Arrowing up and down through cells can lose code that was just typed. + ([#8491](https://github.com/Microsoft/vscode-python/issues/8491)) +1. After pasting code, arrow keys don't navigate in a cell. + ([#8495](https://github.com/Microsoft/vscode-python/issues/8495)) +1. Typing 'z' in a cell causes the cell to disappear. + ([#8594](https://github.com/Microsoft/vscode-python/issues/8594)) + +### Code Health + +1. Add unit tests for src/client/common/process/pythonProcess.ts. + ([#6065](https://github.com/Microsoft/vscode-python/issues/6065)) +1. Remove try...catch around use of vscode.env.shell. + ([#6912](https://github.com/Microsoft/vscode-python/issues/6912)) +1. Test plan needed to be updated to include support for the Notebook Editor. + ([#7593](https://github.com/Microsoft/vscode-python/issues/7593)) +1. Add test step to get correct pywin32 installed with python 3.6 on windows. + ([#7798](https://github.com/Microsoft/vscode-python/issues/7798)) +1. Update Test Explorer icons to match new VS Code icons. + ([#7809](https://github.com/Microsoft/vscode-python/issues/7809)) +1. Fix native editor mime type functional test. + ([#7877](https://github.com/Microsoft/vscode-python/issues/7877)) +1. Fix variable explorer loading test. + ([#7878](https://github.com/Microsoft/vscode-python/issues/7878)) +1. Add telemetry to capture usage of features in the `Notebook Editor` for `Data Science` features. + ([#7908](https://github.com/Microsoft/vscode-python/issues/7908)) +1. Fix debug temporary functional test for Mac / Linux. + ([#7994](https://github.com/Microsoft/vscode-python/issues/7994)) +1. Variable explorer tests failing on nightly. + ([#8124](https://github.com/Microsoft/vscode-python/issues/8124)) +1. Timeout with new waitForMessage in native editor tests. + ([#8255](https://github.com/Microsoft/vscode-python/issues/8255)) +1. Remove code used to track perf of creation classes. + ([#8280](https://github.com/Microsoft/vscode-python/issues/8280)) +1. Update TypeScript to `3.7`. + ([#8395](https://github.com/Microsoft/vscode-python/issues/8395)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [pyparsing](https://pypi.org/project/pyparsing/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.10.1 (22 October 2019) + +### Enhancements + +1. Support other variables for notebookFileRoot besides ${workspaceRoot}. Specifically allow things like ${fileDirName} so that the dir of the first file run in the interactive window is used for the current directory. + ([#4441](https://github.com/Microsoft/vscode-python/issues/4441)) +1. Add command palette commands for native editor (run all cells, run selected cell, add new cell). And remove interactive window commands from contexts where they don't apply. + ([#7800](https://github.com/Microsoft/vscode-python/issues/7800)) +1. Added ability to auto-save chagnes made to the notebook. + ([#7831](https://github.com/Microsoft/vscode-python/issues/7831)) + +### Fixes + +1. Fix regression to allow connection to servers with no token and no password and add functional test for this scenario + ([#7137](https://github.com/Microsoft/vscode-python/issues/7137)) +1. Perf improvements for opening notebooks with more than 100 cells. + ([#7483](https://github.com/Microsoft/vscode-python/issues/7483)) +1. Fix jupyter server startup hang when xeus-cling kernel is installed. + ([#7569](https://github.com/Microsoft/vscode-python/issues/7569)) +1. Make interactive window and native take their fontSize and fontFamily from the settings in VS Code. + ([#7624](https://github.com/Microsoft/vscode-python/issues/7624)) +1. Fix a hang in the Interactive window when connecting guest to host after the host has already started the interactive window. + ([#7638](https://github.com/Microsoft/vscode-python/issues/7638)) +1. Change the default cell marker to '# %%' instead of '#%%' to prevent linter errors in python files with markers. + Also added a new setting to change this - 'python.dataScience.defaultCellMarker'. + ([#7674](https://github.com/Microsoft/vscode-python/issues/7674)) +1. When there's no workspace open, use the directory of the opened file as the root directory for a jupyter session. + ([#7688](https://github.com/Microsoft/vscode-python/issues/7688)) +1. Fix selection and focus not updating when clicking around in a notebook editor. + ([#7802](https://github.com/Microsoft/vscode-python/issues/7802)) +1. Fix add new cell buttons in the notebook editor to give the new cell focus. + ([#7820](https://github.com/Microsoft/vscode-python/issues/7820)) +1. Do not use the PTVSD package version in the folder name for the wheel experiment. + ([#7836](https://github.com/Microsoft/vscode-python/issues/7836)) +1. Prevent updates to the cell text when cell execution of the same cell has commenced or completed. + ([#7844](https://github.com/Microsoft/vscode-python/issues/7844)) +1. Hide the parameters intellisense widget in the `Notebook Editor` when it is not longer required. + ([#7851](https://github.com/Microsoft/vscode-python/issues/7851)) +1. Allow the "Create New Blank Jupyter Notebook" command to be run when the python extension is not loaded yet. + ([#7888](https://github.com/Microsoft/vscode-python/issues/7888)) +1. Ensure the `*.trie` files related to `font kit` npm module are copied into the output directory as part of the `Webpack` bundling operation. + ([#7899](https://github.com/Microsoft/vscode-python/issues/7899)) +1. CTRL+S is not saving a Notebook file. + ([#7904](https://github.com/Microsoft/vscode-python/issues/7904)) +1. When automatically opening the `Notebook Editor`, then ignore uris that do not have a `file` scheme + ([#7905](https://github.com/Microsoft/vscode-python/issues/7905)) +1. Minimize the changes to an ipynb file when saving - preserve metadata and spacing. + ([#7960](https://github.com/Microsoft/vscode-python/issues/7960)) +1. Fix intellisense popping up in the wrong spot when first typing in a cell. + ([#8009](https://github.com/Microsoft/vscode-python/issues/8009)) +1. Fix python.dataScience.maxOutputSize to be honored again. + ([#8010](https://github.com/Microsoft/vscode-python/issues/8010)) +1. Fix markdown disappearing after editing and hitting the escape key. + ([#8045](https://github.com/Microsoft/vscode-python/issues/8045)) + +### Code Health + +1. Add functional tests for notebook editor's use of the variable list. + ([#7369](https://github.com/Microsoft/vscode-python/issues/7369)) +1. More functional tests for the notebook editor. + ([#7372](https://github.com/Microsoft/vscode-python/issues/7372)) +1. Update version of `@types/vscode`. + ([#7832](https://github.com/Microsoft/vscode-python/issues/7832)) +1. Use `Webview.asWebviewUri` to generate a URI for use in the `Webview Panel` instead of hardcoding the resource `vscode-resource`. + ([#7834](https://github.com/Microsoft/vscode-python/issues/7834)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.10.0 (8 October 2019) + +### Enhancements + +1. Experimental version of a native editor for ipynb files. + ([#5959](https://github.com/Microsoft/vscode-python/issues/5959)) +1. Added A/A testing. + ([#6793](https://github.com/Microsoft/vscode-python/issues/6793)) +1. Opt insiders users into beta language server by default. + ([#7108](https://github.com/Microsoft/vscode-python/issues/7108)) +1. Add basic liveshare support for native. + ([#7235](https://github.com/Microsoft/vscode-python/issues/7235)) +1. Change main toolbar to match design spec. + ([#7240](https://github.com/Microsoft/vscode-python/issues/7240)) +1. Telemetry for native editor support. + ([#7252](https://github.com/Microsoft/vscode-python/issues/7252)) +1. Change Variable Explorer to use a sticky button on the main toolbar. + ([#7354](https://github.com/Microsoft/vscode-python/issues/7354)) +1. Add left side navigation bar to native editor. + ([#7377](https://github.com/Microsoft/vscode-python/issues/7377)) +1. Add middle toolbar to a native editor cell. + ([#7378](https://github.com/Microsoft/vscode-python/issues/7378)) +1. Indented the status bar for outputs and changed the background color in the native editor. + ([#7379](https://github.com/Microsoft/vscode-python/issues/7379)) +1. Added a setting `python.experiments.enabled` to enable/disable A/B tests within the extension. + ([#7410](https://github.com/Microsoft/vscode-python/issues/7410)) +1. Add a play button for all users. + ([#7423](https://github.com/Microsoft/vscode-python/issues/7423)) +1. Add a command to show the `Language Server` output panel. + ([#7459](https://github.com/Microsoft/vscode-python/issues/7459)) +1. Make empty notebooks (from File | New File) contain at least one cell. + ([#7516](https://github.com/Microsoft/vscode-python/issues/7516)) +1. Add "clear all output" button to native editor. + ([#7517](https://github.com/Microsoft/vscode-python/issues/7517)) +1. Add support for ptvsd and debug adapter experiments in remote debugging API. + ([#7549](https://github.com/Microsoft/vscode-python/issues/7549)) +1. Support other variables for `notebookFileRoot` besides `${workspaceRoot}`. Specifically allow things like `${fileDirName}` so that the directory of the first file run in the interactive window is used for the current directory. + ([#4441](https://github.com/Microsoft/vscode-python/issues/4441)) + +### Fixes + +1. Replaced occurrences of `pep8` with `pycodestyle.` + All mentions of pep8 have been replaced with pycodestyle. + Add script to replace outdated settings with the new ones in user settings.json + - python.linting.pep8Args -> python.linting.pycodestyleArgs + - python.linting.pep8CategorySeverity.E -> python.linting.pycodestyleCategorySeverity.E + - python.linting.pep8CategorySeverity.W -> python.linting.pycodestyleCategorySeverity.W + - python.linting.pep8Enabled -> python.linting.pycodestyleEnabled + - python.linting.pep8Path -> python.linting.pycodestylePath + - (thanks [Marsfan](https://github.com/Marsfan)) + ([#410](https://github.com/Microsoft/vscode-python/issues/410)) +1. Do not change `foreground` colors in test statusbar. + ([#4387](https://github.com/Microsoft/vscode-python/issues/4387)) +1. Set the `__file__` variable whenever running code so that `__file__` usage works in the interactive window. + ([#5459](https://github.com/Microsoft/vscode-python/issues/5459)) +1. Ensure Windows Store install of Python is displayed in the statusbar. + ([#5926](https://github.com/Microsoft/vscode-python/issues/5926)) +1. Fix loging for determining python path from workspace of active text editor (thanks [Eric Bajumpaa (@SteelPhase)](https://github.com/SteelPhase)). + ([#6282](https://github.com/Microsoft/vscode-python/issues/6282)) +1. Changed the way scrolling is treated. Now we only check for the position of the scroll, the size of the cell won't matter. + Still the interactive window will snap to the bottom if you already are at the bottom, and will stay in place if you are not. Like a chat window. + Tested to work with: + - regular code + - dataframes + - big and regular plots + Turned the check of the scroll at the bottom from checking equal to checking a range to make it work with fractions. + ([#6580](https://github.com/Microsoft/vscode-python/issues/6580)) +1. Changed the name of the setting 'Run Magic Commands' to 'Run Startup Commands' to avoid confusion. + ([#6842](https://github.com/Microsoft/vscode-python/issues/6842)) +1. Fix the debugger being installed even when available from the VSCode install. + ([#6907](https://github.com/Microsoft/vscode-python/issues/6907)) +1. Fixes to detection of shell. + ([#6928](https://github.com/Microsoft/vscode-python/issues/6928)) +1. Delete the old session immediately after session restart instead of on close. + ([#6975](https://github.com/Microsoft/vscode-python/issues/6975)) +1. Add support for the new JUnit XML format used by pytest 5.1+. + ([#6990](https://github.com/Microsoft/vscode-python/issues/6990)) +1. Set a content security policy on webviews. + ([#7007](https://github.com/Microsoft/vscode-python/issues/7007)) +1. Fix regression to allow connection to servers with no token and no password and add functional test for this scenario. + ([#7137](https://github.com/Microsoft/vscode-python/issues/7137)) +1. Resolve variables such as `${workspaceFolder}` in the `envFile` setting of `launch.json`. + ([#7210](https://github.com/Microsoft/vscode-python/issues/7210)) +1. Fixed A/B testing sampling. + ([#7218](https://github.com/Microsoft/vscode-python/issues/7218)) +1. Added commands for 'dd', 'ctrl + enter', 'alt + enter', 'a', 'b', 'j', 'k' in the native Editor to behave just like JupyterLabs. + ([#7229](https://github.com/Microsoft/vscode-python/issues/7229)) +1. Add support for CTRL+S when the native editor has input focus (best we can do without true editor support) + Also fix issue with opening two or more not gaining focus correctly. + ([#7238](https://github.com/Microsoft/vscode-python/issues/7238)) +1. Fix monaco editor layout perf. + ([#7241](https://github.com/Microsoft/vscode-python/issues/7241)) +1. Fix 'history' in the input box for the interactive window to work again. Up arrow and down arrow should now scroll through the things already typed in. + ([#7253](https://github.com/Microsoft/vscode-python/issues/7253)) +1. Fix plot viewer to allow exporting again. + ([#7257](https://github.com/Microsoft/vscode-python/issues/7257)) +1. Make ipynb files auto save on shutting down VS code as our least bad option at the moment. + ([#7258](https://github.com/Microsoft/vscode-python/issues/7258)) +1. Update icons to newer look. + ([#7261](https://github.com/Microsoft/vscode-python/issues/7261)) +1. The native editor will now wrap all its content instead of showing a horizontal scrollbar. + ([#7272](https://github.com/Microsoft/vscode-python/issues/7272)) +1. Deprecate the 'runMagicCommands' datascience setting. + ([#7294](https://github.com/Microsoft/vscode-python/issues/7294)) +1. Fix white icon background and finish update all icons to new style. + ([#7302](https://github.com/Microsoft/vscode-python/issues/7302)) +1. Fixes to display `Python` specific debug configurations in `launch.json`. + ([#7304](https://github.com/Microsoft/vscode-python/issues/7304)) +1. Fixed intellisense support on the native editor. + ([#7316](https://github.com/Microsoft/vscode-python/issues/7316)) +1. Fix double opening an ipynb file to still use the native editor. + ([#7318](https://github.com/Microsoft/vscode-python/issues/7318)) +1. 'j' and 'k' were reversed for navigating through the native editor. + ([#7330](https://github.com/Microsoft/vscode-python/issues/7330)) +1. 'a' keyboard shortcut doesn't add a cell above if current cell is the first. + ([#7334](https://github.com/Microsoft/vscode-python/issues/7334)) +1. Add the 'add cell' line between cells, on cells, and at the bottom and top. + ([#7362](https://github.com/Microsoft/vscode-python/issues/7362)) +1. Runtime errors cause the run button to disappear. + ([#7370](https://github.com/Microsoft/vscode-python/issues/7370)) +1. Surface jupyter notebook search errors to the user. + ([#7392](https://github.com/Microsoft/vscode-python/issues/7392)) +1. Allow cells to be re-executed on second open of an ipynb file. + ([#7417](https://github.com/Microsoft/vscode-python/issues/7417)) +1. Implement dirty file tracking for notebooks so that on reopening of VS code they are shown in the dirty state. + Canceling the save will get them back to their on disk state. + ([#7418](https://github.com/Microsoft/vscode-python/issues/7418)) +1. Make ipynb files change to dirty when moving/deleting/changing cells. + ([#7439](https://github.com/Microsoft/vscode-python/issues/7439)) +1. Initial collapse / expand state broken by native liveshare work / gather. + ([#7445](https://github.com/Microsoft/vscode-python/issues/7445)) +1. Converting a native markdown cell to code removes the markdown source. + ([#7446](https://github.com/Microsoft/vscode-python/issues/7446)) +1. Text is cut off on the right hand side of a notebook editor. + ([#7472](https://github.com/Microsoft/vscode-python/issues/7472)) +1. Added a prompt asking users to enroll back in the insiders program. + ([#7473](https://github.com/Microsoft/vscode-python/issues/7473)) +1. Fix collapse bar and add new line spacing for the native editor. + ([#7489](https://github.com/Microsoft/vscode-python/issues/7489)) +1. Add new cell top most toolbar button should take selection into account when adding a cell. + ([#7490](https://github.com/Microsoft/vscode-python/issues/7490)) +1. Move up and move down arrows in native editor are different sizes. + ([#7494](https://github.com/Microsoft/vscode-python/issues/7494)) +1. Fix jedi intellisense in the notebook editor to be performant. + ([#7497](https://github.com/Microsoft/vscode-python/issues/7497)) +1. The add cell line should have a hover cursor. + ([#7508](https://github.com/Microsoft/vscode-python/issues/7508)) +1. Toolbar in the middle of a notebook cell should show up on hover. + ([#7515](https://github.com/Microsoft/vscode-python/issues/7515)) +1. 'z' key will now undo cell deletes/adds/moves. + ([#7518](https://github.com/Microsoft/vscode-python/issues/7518)) +1. Rename and restyle the save as python file button. + ([#7519](https://github.com/Microsoft/vscode-python/issues/7519)) +1. Fix for changing a file in the status bar to a notebook/jupyter file to open the new native notebook editor. + ([#7521](https://github.com/Microsoft/vscode-python/issues/7521)) +1. Running a cell by clicking the mouse should behave like shift+enter and move to the next cell (or add one to the bottom). + ([#7522](https://github.com/Microsoft/vscode-python/issues/7522)) +1. Output color makes a text only notebook with a lot of cells hard to read. Change output color to be the same as the background like Jupyter does. + ([#7526](https://github.com/Microsoft/vscode-python/issues/7526)) +1. Fix data viewer sometimes showing no data at all (especially on small datasets). + ([#7530](https://github.com/Microsoft/vscode-python/issues/7530)) +1. First run of run all cells doesn't run the first cell first. + ([#7558](https://github.com/Microsoft/vscode-python/issues/7558)) +1. Saving an untitled notebook editor doesn't change the tab to have the new file name. + ([#7561](https://github.com/Microsoft/vscode-python/issues/7561)) +1. Closing and reopening a notebook doesn't reset the execution count. + ([#7565](https://github.com/Microsoft/vscode-python/issues/7565)) +1. After restarting kernel, variables don't reset in the notebook editor. + ([#7573](https://github.com/Microsoft/vscode-python/issues/7573)) +1. CTRL+1/CTRL+2 had stopped working in the interactive window. + ([#7597](https://github.com/Microsoft/vscode-python/issues/7597)) +1. Ensure the insiders prompt only shows once. + ([#7606](https://github.com/Microsoft/vscode-python/issues/7606)) +1. Added prompt to flip "inheritEnv" setting to false to fix conda activation issue. + ([#7607](https://github.com/Microsoft/vscode-python/issues/7607)) +1. Toggling line numbers and output was not possible in the notebook editor. + ([#7610](https://github.com/Microsoft/vscode-python/issues/7610)) +1. Align execution count with first line of a cell. + ([#7611](https://github.com/Microsoft/vscode-python/issues/7611)) +1. Fix debugging cells to work when the python executable has spaces in the path. + ([#7627](https://github.com/Microsoft/vscode-python/issues/7627)) +1. Add switch channel commands into activationEvents to fix `command 'Python.swichToDailyChannel' not found`. + ([#7636](https://github.com/Microsoft/vscode-python/issues/7636)) +1. Goto cell code lens was not scrolling. + ([#7639](https://github.com/Microsoft/vscode-python/issues/7639)) +1. Make interactive window and native take their `fontSize` and `fontFamily` from the settings in VS Code. + ([#7624](https://github.com/Microsoft/vscode-python/issues/7624)) +1. Fix a hang in the Interactive window when connecting guest to host after the host has already started the interactive window. + ([#7638](https://github.com/Microsoft/vscode-python/issues/7638)) +1. When there's no workspace open, use the directory of the opened file as the root directory for a Jupyter session. + ([#7688](https://github.com/Microsoft/vscode-python/issues/7688)) +1. Allow the language server to pick a default caching mode. + ([#7821](https://github.com/Microsoft/vscode-python/issues/7821)) + +### Code Health + +1. Use jsonc-parser instead of strip-json-comments. + (thanks [Mikhail Bulash](https://github.com/mikeroll/)) + ([#4819](https://github.com/Microsoft/vscode-python/issues/4819)) +1. Remove `donjamayanne.jupyter` integration. + (thanks [Mikhail Bulash](https://github.com/mikeroll/)) + ([#6052](https://github.com/Microsoft/vscode-python/issues/6052)) +1. Drop `python.updateSparkLibrary` command. + (thanks [Mikhail Bulash](https://github.com/mikeroll/)) + ([#6091](https://github.com/Microsoft/vscode-python/issues/6091)) +1. Re-enabled smoke tests (refactored in `node.js` with [puppeteer](https://github.com/GoogleChrome/puppeteer)). + ([#6511](https://github.com/Microsoft/vscode-python/issues/6511)) +1. Handle situations where language client is disposed earlier than expected. + ([#6865](https://github.com/Microsoft/vscode-python/issues/6865)) +1. Put Data science functional tests that use real jupyter into their own test pipeline. + ([#7066](https://github.com/Microsoft/vscode-python/issues/7066)) +1. Send telemetry for what language server is chosen. + ([#7109](https://github.com/Microsoft/vscode-python/issues/7109)) +1. Add telemetry to measure debugger start up performance. + ([#7332](https://github.com/Microsoft/vscode-python/issues/7332)) +1. Decouple the DS location tracker from the debug session telemetry. + ([#7352](https://github.com/Microsoft/vscode-python/issues/7352)) +1. Test scaffolding for notebook editor. + ([#7367](https://github.com/Microsoft/vscode-python/issues/7367)) +1. Add functional tests for notebook editor's use of the variable list. + ([#7369](https://github.com/Microsoft/vscode-python/issues/7369)) +1. Tests for the notebook editor for different mime types. + ([#7371](https://github.com/Microsoft/vscode-python/issues/7371)) +1. Split Cell class for different views. + ([#7376](https://github.com/Microsoft/vscode-python/issues/7376)) +1. Refactor Azure Pipelines to use stages. + ([#7431](https://github.com/Microsoft/vscode-python/issues/7431)) +1. Add unit tests to guarantee that the extension version in the main branch has the '-dev' suffix. + ([#7471](https://github.com/Microsoft/vscode-python/issues/7471)) +1. Add a smoke test for the `Interactive Window`. + ([#7653](https://github.com/Microsoft/vscode-python/issues/7653)) +1. Download PTVSD wheels (for the new PTVSD) as part of CI. + ([#7028](https://github.com/Microsoft/vscode-python/issues/7028)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.9.1 (6 September 2019) + +### Fixes + +1. Fixes to automatic scrolling on the interactive window. + ([#6580](https://github.com/Microsoft/vscode-python/issues/6580)) + +## 2019.9.0 (3 September 2019) + +### Enhancements + +1. Get "select virtual environment for the workspace" prompt to show up regardless of pythonpath setting. + ([#5499](https://github.com/Microsoft/vscode-python/issues/5499)) +1. Changes to telemetry with regards to discovery of python environments. + ([#5593](https://github.com/Microsoft/vscode-python/issues/5593)) +1. Update Jedi to 0.15.1 and parso to 0.5.1. + ([#6294](https://github.com/Microsoft/vscode-python/issues/6294)) +1. Moved Language Server logging to its own output channel. + ([#6559](https://github.com/Microsoft/vscode-python/issues/6559)) +1. Interactive window will only snap to the bottom if the user is already in the bottom, like a chat window. + ([#6580](https://github.com/Microsoft/vscode-python/issues/6580)) +1. Add debug command code lenses when in debug mode. + ([#6672](https://github.com/Microsoft/vscode-python/issues/6672)) +1. Implemented prompt for survey. + ([#6752](https://github.com/Microsoft/vscode-python/issues/6752)) +1. Add code gathering tools. + ([#6810](https://github.com/Microsoft/vscode-python/issues/6810)) +1. Added a setting called 'Run Magic Commands'. The input should be python code to be executed when the interactive window is loading. + ([#6842](https://github.com/Microsoft/vscode-python/issues/6842)) +1. Added a setting so the user can decide if they want the debugger to debug only their code, or also debug external libraries. + ([#6870](https://github.com/Microsoft/vscode-python/issues/6870)) +1. Implemented prompt for survey using A/B test framework. + ([#6957](https://github.com/Microsoft/vscode-python/issues/6957)) + +### Fixes + +1. Delete the old session immediatly after session restart instead of on close + ([#6975](https://github.com/Microsoft/vscode-python/issues/6975)) +1. Add support for the "pathMappings" setting in "launch" debug configs. + ([#3568](https://github.com/Microsoft/vscode-python/issues/3568)) +1. Supports error codes like ABC123 as used in plugins. + ([#4074](https://github.com/Microsoft/vscode-python/issues/4074)) +1. Fixes to insertion of commas when inserting generated debug configurations in `launch.json`. + ([#5531](https://github.com/Microsoft/vscode-python/issues/5531)) +1. Fix code lenses shown for pytest. + ([#6303](https://github.com/Microsoft/vscode-python/issues/6303)) +1. Make data viewer change row height according to font size in settings. + ([#6614](https://github.com/Microsoft/vscode-python/issues/6614)) +1. Fix miniconda environments to work. + ([#6802](https://github.com/Microsoft/vscode-python/issues/6802)) +1. Drop dedent-on-enter for "return" statements. It will be addressed in https://github.com/microsoft/vscode-python/issues/6564. + ([#6813](https://github.com/Microsoft/vscode-python/issues/6813)) +1. Show PTVSD exceptions to the user. + ([#6818](https://github.com/Microsoft/vscode-python/issues/6818)) +1. Tweaked message for restarting VS Code to use a Python Extension insider build + (thanks [Marsfan](https://github.com/Marsfan)). + ([#6838](https://github.com/Microsoft/vscode-python/issues/6838)) +1. Do not execute empty code cells or render them in the interactive window when sent from the editor or input box. + ([#6839](https://github.com/Microsoft/vscode-python/issues/6839)) +1. Fix failing functional tests (for pytest) in the extension. + ([#6940](https://github.com/Microsoft/vscode-python/issues/6940)) +1. Fix ptvsd typo in descriptions. + ([#7097](https://github.com/Microsoft/vscode-python/issues/7097)) + +### Code Health + +1. Update the message and the link displayed when `Language Server` isn't supported. + ([#5969](https://github.com/Microsoft/vscode-python/issues/5969)) +1. Normalize path separators in stack traces. + ([#6460](https://github.com/Microsoft/vscode-python/issues/6460)) +1. Update `package.json` to define supported languages for breakpoints. + Update telemetry code to hardcode Telemetry Key in code (removed from `package.json`). + ([#6469](https://github.com/Microsoft/vscode-python/issues/6469)) +1. Functional tests for DataScience Error Handler. + ([#6697](https://github.com/Microsoft/vscode-python/issues/6697)) +1. Move .env file handling into the extension. This is in preparation to switch to the out-of-proc debug adapter from ptvsd. + ([#6770](https://github.com/Microsoft/vscode-python/issues/6770)) +1. Track enablement of a test framework. + ([#6783](https://github.com/Microsoft/vscode-python/issues/6783)) +1. Track how code was sent to the terminal (via `command` or `UI`). + ([#6801](https://github.com/Microsoft/vscode-python/issues/6801)) +1. Upload coverage reports to [codecov](https://codecov.io/gh/microsoft/vscode-python). + ([#6938](https://github.com/Microsoft/vscode-python/issues/6938)) +1. Bump version of [PTVSD](https://pypi.org/project/ptvsd/) to `4.3.2`. + + - Fix an issue with Jump to cursor command. [#1667](https://github.com/microsoft/ptvsd/issues/1667) + - Fix "Unable to find threadStateIndex for the current thread" message in terminal. [#1587](https://github.com/microsoft/ptvsd/issues/1587) + - Fixes crash when using python 3.7.4. [#1688](https://github.com/microsoft/ptvsd/issues/1688) + ([#6961](https://github.com/Microsoft/vscode-python/issues/6961)) + +1. Move nightly functional tests to use mock jupyter and create a new pipeline for flakey tests which use real jupyter. + ([#7066](https://github.com/Microsoft/vscode-python/issues/7066)) +1. Corrected spelling of name for method to be `hasConfigurationFileInWorkspace`. + ([#7072](https://github.com/Microsoft/vscode-python/issues/7072)) +1. Fix functional test failures due to new WindowsStoreInterpreter addition. + ([#7081](https://github.com/Microsoft/vscode-python/issues/7081)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.8.0 (6 August 2019) + +### Enhancements + +1. Added ability to auto update Insiders build of extension. + ([#2772](https://github.com/Microsoft/vscode-python/issues/2772)) +1. Add an icon for the "Run Python File in Terminal" command. + ([#5321](https://github.com/Microsoft/vscode-python/issues/5321)) +1. Hook up ptvsd debugger to Jupyter UI. + ([#5900](https://github.com/Microsoft/vscode-python/issues/5900)) +1. Improved keyboard and screen reader support for the data explorer. + ([#6019](https://github.com/Microsoft/vscode-python/issues/6019)) +1. Provide code mapping service for debugging cells. + ([#6318](https://github.com/Microsoft/vscode-python/issues/6318)) +1. Change copy back to code button in the interactive window to insert wherever the current selection is. + ([#6350](https://github.com/Microsoft/vscode-python/issues/6350)) +1. Add new 'goto cell' code lens on every cell that is run from a file. + ([#6359](https://github.com/Microsoft/vscode-python/issues/6359)) +1. Allow for cancelling all cells when an error occurs. Backed by 'stopOnError' setting. + ([#6366](https://github.com/Microsoft/vscode-python/issues/6366)) +1. Added Code Lens and Snippet to add new cell. + ([#6367](https://github.com/Microsoft/vscode-python/issues/6367)) +1. Support hitting breakpoints in actual source code for interactive window debugging. + ([#6376](https://github.com/Microsoft/vscode-python/issues/6376)) +1. Give the option to install ptvsd if user is missing it and tries to debug. + ([#6378](https://github.com/Microsoft/vscode-python/issues/6378)) +1. Add support for remote debugging of Jupyter cells. + ([#6379](https://github.com/Microsoft/vscode-python/issues/6379)) +1. Make the input box more visible to new users. + ([#6381](https://github.com/Microsoft/vscode-python/issues/6381)) +1. Add feature flag `python.dataScience.magicCommandsAsComments` so linters and other tools can work with them. + (thanks [Janosh Riebesell](https://github.com/janosh)) + ([#6408](https://github.com/Microsoft/vscode-python/issues/6408)) +1. Support break on enter for debugging a cell. + ([#6449](https://github.com/Microsoft/vscode-python/issues/6449)) +1. instead of asking the user to select an installer, we now autodetect the environment being used, and use that installer. + ([#6569](https://github.com/Microsoft/vscode-python/issues/6569)) +1. Remove "Debug cell" action from data science code lenses for markdown cells. + (thanks [Janosh Riebesell](https://github.com/janosh)) + ([#6588](https://github.com/Microsoft/vscode-python/issues/6588)) +1. Add debug command code lenses when in debug mode + ([#6672](https://github.com/Microsoft/vscode-python/issues/6672)) + +### Fixes + +1. Fix `executeInFileDir` for when a file is not in a workspace. + (thanks [Bet4](https://github.com/bet4it/)) + ([#1062](https://github.com/Microsoft/vscode-python/issues/1062)) +1. Fix indentation after string literals containing escaped characters. + ([#4241](https://github.com/Microsoft/vscode-python/issues/4241)) +1. The extension will now prompt to auto install jupyter in case its not found. + ([#5682](https://github.com/Microsoft/vscode-python/issues/5682)) +1. Append `--allow-prereleases` to black installation command so pipenv can properly resolve it. + ([#5756](https://github.com/Microsoft/vscode-python/issues/5756)) +1. Remove existing positional arguments when running single pytest tests. + ([#5757](https://github.com/Microsoft/vscode-python/issues/5757)) +1. Fix shift+enter to work when code lens are turned off. + ([#5879](https://github.com/Microsoft/vscode-python/issues/5879)) +1. Prompt to insall test framework only if test frame is not already installed. + ([#5919](https://github.com/Microsoft/vscode-python/issues/5919)) +1. Trim stream text output at the server to prevent sending massive strings of overwritten data. + ([#6001](https://github.com/Microsoft/vscode-python/issues/6001)) +1. Detect `shell` in Visual Studio Code using the Visual Studio Code API. + ([#6050](https://github.com/Microsoft/vscode-python/issues/6050)) +1. Make long running output not crash the extension host. Also improve perf of streaming. + ([#6222](https://github.com/Microsoft/vscode-python/issues/6222)) +1. Opting out of telemetry correctly opts out of A/B testing. + ([#6270](https://github.com/Microsoft/vscode-python/issues/6270)) +1. Add error messages if data_rate_limit is exceeded on remote (or local) connection. + ([#6273](https://github.com/Microsoft/vscode-python/issues/6273)) +1. Add pytest-xdist's -n option to the list of supported pytest options. + ([#6293](https://github.com/Microsoft/vscode-python/issues/6293)) +1. Simplify the import regex to minimize performance overhead. + ([#6319](https://github.com/Microsoft/vscode-python/issues/6319)) +1. Clarify regexes used for decreasing indentation. + ([#6333](https://github.com/Microsoft/vscode-python/issues/6333)) +1. Add new plot viewer button images and fix button colors in different themes. + ([#6336](https://github.com/Microsoft/vscode-python/issues/6336)) +1. Update telemetry property name for Jedi memory usage. + ([#6339](https://github.com/Microsoft/vscode-python/issues/6339)) +1. Fix png scaling on non standard DPI. Add 'enablePlotViewer' setting to allow user to render PNGs instead of SVG files. + ([#6344](https://github.com/Microsoft/vscode-python/issues/6344)) +1. Do best effort to download the experiments and use it in the very first session only. + ([#6348](https://github.com/Microsoft/vscode-python/issues/6348)) +1. Linux can pick the wrong kernel to use when starting the interactive window. + ([#6375](https://github.com/Microsoft/vscode-python/issues/6375)) +1. Add missing keys for data science interactive window button tooltips in `package.nls.json`. + ([#6386](https://github.com/Microsoft/vscode-python/issues/6386)) +1. Fix overwriting of cwd in the path list when discovering tests. + ([#6417](https://github.com/Microsoft/vscode-python/issues/6417)) +1. Fixes a bug in pytest test discovery. + (thanks Rainer Dreyer) + ([#6463](https://github.com/Microsoft/vscode-python/issues/6463)) +1. Fix debugging to work on restarting the jupyter kernel. + ([#6502](https://github.com/Microsoft/vscode-python/issues/6502)) +1. Escape key in the interactive window moves to the delete button when auto complete is open. Escape should only move when no autocomplete is open. + ([#6507](https://github.com/Microsoft/vscode-python/issues/6507)) +1. Render plots as png, but save an svg for exporting/image viewing. Speeds up plot rendering. + ([#6526](https://github.com/Microsoft/vscode-python/issues/6526)) +1. Import get_ipython at the start of each imported jupyter notebook if there are line magics in the file + ([#6574](https://github.com/Microsoft/vscode-python/issues/6574)) +1. Fix a problem where we retrieved and rendered old codelenses for multiple imports of jupyter notebooks if cells in the resultant import file were executed without saving the file to disk. + ([#6582](https://github.com/Microsoft/vscode-python/issues/6582)) +1. PTVSD install for jupyter debugging should check version without actually importing into the jupyter kernel. + ([#6592](https://github.com/Microsoft/vscode-python/issues/6592)) +1. Fix pandas version parsing to handle strings. + ([#6595](https://github.com/Microsoft/vscode-python/issues/6595)) +1. Unpin the version of ptvsd in the install and add `-U`. + ([#6718](https://github.com/Microsoft/vscode-python/issues/6718)) +1. Fix stepping when more than one blank line at the end of a cell. + ([#6719](https://github.com/Microsoft/vscode-python/issues/6719)) +1. Render plots as png, but save an svg for exporting/image viewing. Speeds up plot rendering. + ([#6724](https://github.com/Microsoft/vscode-python/issues/6724)) +1. Fix random occurrences of output not concatenating correctly in the interactive window. + ([#6728](https://github.com/Microsoft/vscode-python/issues/6728)) +1. In order to debug without '#%%' defined in a file, support a Debug Entire File. + ([#6730](https://github.com/Microsoft/vscode-python/issues/6730)) +1. Add support for "Run Below" back. + ([#6737](https://github.com/Microsoft/vscode-python/issues/6737)) +1. Fix the 'Variables not available while debugging' message to be more descriptive. + ([#6740](https://github.com/Microsoft/vscode-python/issues/6740)) +1. Make breakpoints on enter always be the case unless 'stopOnFirstLineWhileDebugging' is set. + ([#6743](https://github.com/Microsoft/vscode-python/issues/6743)) +1. Remove Debug Cell and Run Cell from the command palette. They should both be 'Debug Current Cell' and 'Run Current Cell' + ([#6754](https://github.com/Microsoft/vscode-python/issues/6754)) +1. Make the dataviewer open a window much faster. Total load time is the same, but initial response is much faster. + ([#6729](https://github.com/Microsoft/vscode-python/issues/6729)) +1. Debugging an untitled file causes an error 'Untitled-1 cannot be opened'. + ([#6738](https://github.com/Microsoft/vscode-python/issues/6738)) +1. Eliminate 'History\_\' from the problems list when using the interactive panel. + ([#6748](https://github.com/Microsoft/vscode-python/issues/6748)) + +### Code Health + +1. Log processes executed behind the scenes in the extension output panel. + ([#1131](https://github.com/Microsoft/vscode-python/issues/1131)) +1. Specify `pyramid.scripts.pserve` when creating a debug configuration for Pyramid + apps instead of trying to calculate the location of the `pserve` command. + ([#2427](https://github.com/Microsoft/vscode-python/issues/2427)) +1. UI Tests using [selenium](https://selenium-python.readthedocs.io/index.html) & [behave](https://behave.readthedocs.io/en/latest/). + ([#4692](https://github.com/Microsoft/vscode-python/issues/4692)) +1. Upload coverage reports to [coveralls](https://coveralls.io/github/microsoft/vscode-python). + ([#5999](https://github.com/Microsoft/vscode-python/issues/5999)) +1. Upgrade Jedi to version 0.13.3. + ([#6013](https://github.com/Microsoft/vscode-python/issues/6013)) +1. Add unit tests for `client/activation/serviceRegistry.ts`. + ([#6163](https://github.com/Microsoft/vscode-python/issues/6163)) +1. Remove `test.ipynb` from the root folder. + ([#6212](https://github.com/Microsoft/vscode-python/issues/6212)) +1. Fail the `smoke tests` CI job when the smoke tests fail. + ([#6253](https://github.com/Microsoft/vscode-python/issues/6253)) +1. Add a bunch of perf measurements to telemetry. + ([#6283](https://github.com/Microsoft/vscode-python/issues/6283)) +1. Retry failing debugger test (retry due to intermittent issues on `Azure Pipelines`). + ([#6322](https://github.com/Microsoft/vscode-python/issues/6322)) +1. Update version of `isort` to `4.3.21`. + ([#6369](https://github.com/Microsoft/vscode-python/issues/6369)) +1. Functional test for debugging jupyter cells. + ([#6377](https://github.com/Microsoft/vscode-python/issues/6377)) +1. Consolidate telemetry. + ([#6451](https://github.com/Microsoft/vscode-python/issues/6451)) +1. Removed npm package `vscode`, and added to use `vscode-test` and `@types/vscode` (see [here](https://code.visualstudio.com/updates/v1_36#_splitting-vscode-package-into-typesvscode-and-vscodetest) for more info). + ([#6456](https://github.com/Microsoft/vscode-python/issues/6456)) +1. Fix the variable explorer exclude test to be less strict. + ([#6525](https://github.com/Microsoft/vscode-python/issues/6525)) +1. Merge ArgumentsHelper unit tests into one file. + ([#6583](https://github.com/Microsoft/vscode-python/issues/6583)) +1. Fix jupyter remote tests to respect new notebook 6.0 output format. + ([#6625](https://github.com/Microsoft/vscode-python/issues/6625)) +1. Unit Tests for DataScience Error Handler. + ([#6670](https://github.com/Microsoft/vscode-python/issues/6670)) +1. Fix DataExplorer tests after accessibility fixes. + ([#6711](https://github.com/Microsoft/vscode-python/issues/6711)) +1. Bump version of [PTVSD](https://pypi.org/project/ptvsd/) to 4.3.0. + ([#6771](https://github.com/Microsoft/vscode-python/issues/6771)) + - Support for Jupyter debugging + - Support for ipython cells + - API to enable and disable tracing via ptvsd.tracing + - ptvsd.enable_attach accepts address=('localhost', 0) and returns server port + - Known issue: Unable to find threadStateIndex for the current thread. curPyThread ([#11587](https://github.com/microsoft/ptvsd/issues/1587)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort](https://pypi.org/project/isort/) +- [jedi](https://pypi.org/project/jedi/) + and [parso](https://pypi.org/project/parso/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.6.1 (9 July 2019) + +### Fixes + +1. Fixes to A/B testing. + ([#6400](https://github.com/microsoft/vscode-python/issues/6400)) + +## 2019.6.0 (25 June 2019) + +### Enhancements + +1. Dedent selected code before sending it to the terminal. + ([#2837](https://github.com/Microsoft/vscode-python/issues/2837)) +1. Allow password for remote authentication. + ([#3624](https://github.com/Microsoft/vscode-python/issues/3624)) +1. Add support for sub process debugging, when debugging tests. + ([#4525](https://github.com/Microsoft/vscode-python/issues/4525)) +1. Change title of `Discover Tests` to `Discovering` when discovering tests. + ([#4562](https://github.com/Microsoft/vscode-python/issues/4562)) +1. Add an extra viewer for plots in the interactive window. + ([#4967](https://github.com/Microsoft/vscode-python/issues/4967)) +1. Allow for self signed certificates for jupyter connections. + ([#4987](https://github.com/Microsoft/vscode-python/issues/4987)) +1. Add support for A/B testing and staged rollouts of new functionality. + ([#5042](https://github.com/Microsoft/vscode-python/issues/5042)) +1. Removed `--nothreading` flag from the `Django` debug configuration. + ([#5116](https://github.com/Microsoft/vscode-python/issues/5116)) +1. Test Explorer : Remove time from all nodes except the tests. + ([#5120](https://github.com/Microsoft/vscode-python/issues/5120)) +1. Add support for a copy back to source. + ([#5286](https://github.com/Microsoft/vscode-python/issues/5286)) +1. Add visual separation between the variable explorer and the rest of the Interactive Window content. + ([#5389](https://github.com/Microsoft/vscode-python/issues/5389)) +1. Changes placeholder label in testConfigurationManager.ts from 'Select the directory containing the unit tests' to 'Select the directory containing the tests'. + (thanks [James Flynn](https://github.com/james-flynn-ie/)) + ([#5602](https://github.com/Microsoft/vscode-python/issues/5602)) +1. Updated labels in File > Preferences > Settings. It now states 'Pytest' where it stated 'Py Test'. + (thanks [James Flynn](https://github.com/james-flynn-ie/)) + ([#5603](https://github.com/Microsoft/vscode-python/issues/5603)) +1. Updated label for "Enable unit testing for Pytest" to remove the word "unit". + (thanks [James Flynn](https://github.com/james-flynn-ie/)) + ([#5604](https://github.com/Microsoft/vscode-python/issues/5604)) +1. Importing a notebook should show the output of the notebook in the Python Interactive window. This feature can be turned off with the 'previewImportedNotebooksInInteractivePane' setting. + ([#5675](https://github.com/Microsoft/vscode-python/issues/5675)) +1. Add flag to auto preview an ipynb file when opened. + ([#5790](https://github.com/Microsoft/vscode-python/issues/5790)) +1. Change pytest description from configuration menu. + ([#5832](https://github.com/Microsoft/vscode-python/issues/5832)) +1. Support faster restart of the kernel by creating two kernels (two python processes running under the covers). + ([#5876](https://github.com/Microsoft/vscode-python/issues/5876)) +1. Allow a 'Dont ask me again' option for restarting the kernel. + ([#5951](https://github.com/Microsoft/vscode-python/issues/5951)) +1. Added experiment to always display the test explorer. + ([#6211](https://github.com/Microsoft/vscode-python/issues/6211)) + +### Fixes + +1. Added support for activation of conda environments in `powershell`. + ([#668](https://github.com/Microsoft/vscode-python/issues/668)) +1. Provide `pathMappings` to debugger when attaching to child processes. + ([#3568](https://github.com/Microsoft/vscode-python/issues/3568)) +1. Add virtualenvwrapper default virtual environment location to the `python.venvFolders` config setting. + ([#4642](https://github.com/Microsoft/vscode-python/issues/4642)) +1. Advance to the next cell if cursor is in the current cell and user clicks 'Run Cell'. + ([#5067](https://github.com/Microsoft/vscode-python/issues/5067)) +1. Fix localhost path mappings to lowercase the drive letter on Windows. + ([#5362](https://github.com/Microsoft/vscode-python/issues/5362)) +1. Fix import/export paths to be escaped on windows. + ([#5386](https://github.com/Microsoft/vscode-python/issues/5386)) +1. Support loading larger dataframes in the dataviewer (anything more than 1000 columns will still be slow, but won't crash). + ([#5469](https://github.com/Microsoft/vscode-python/issues/5469)) +1. Fix magics running from a python file. + ([#5537](https://github.com/Microsoft/vscode-python/issues/5537)) +1. Change scrolling to not animate to workaround async updates breaking the animation. + ([#5560](https://github.com/Microsoft/vscode-python/issues/5560)) +1. Add support for opening hyperlinks from the interactive window. + ([#5630](https://github.com/Microsoft/vscode-python/issues/5630)) +1. Remove extra padding in the dataviewer. + ([#5653](https://github.com/Microsoft/vscode-python/issues/5653)) +1. Add 'Add empty cell to file' command. Shortcut for having to type '#%%'. + ([#5667](https://github.com/Microsoft/vscode-python/issues/5667)) +1. Add 'ctrl+enter' as a keyboard shortcut for run current cell (runs without advancing) + ([#5673](https://github.com/Microsoft/vscode-python/issues/5673)) +1. Adjust input box prompt to look more an IPython console prompt. + ([#5729](https://github.com/Microsoft/vscode-python/issues/5729)) +1. Jupyter-notebook exists after shutdown. + ([#5731](https://github.com/Microsoft/vscode-python/issues/5731)) +1. Fix horizontal scrolling in the Interactive Window. + ([#5734](https://github.com/Microsoft/vscode-python/issues/5734)) +1. Fix problem with using up/down arrows in autocomplete. + ([#5774](https://github.com/Microsoft/vscode-python/issues/5774)) +1. Fix latex and markdown scrolling. + ([#5775](https://github.com/Microsoft/vscode-python/issues/5775)) +1. Add support for jupyter controls that clear. + ([#5801](https://github.com/Microsoft/vscode-python/issues/5801)) +1. Fix up arrow on signature help closing the help. + ([#5813](https://github.com/Microsoft/vscode-python/issues/5813)) +1. Make the interactive window respect editor cursor and blink style. + ([#5814](https://github.com/Microsoft/vscode-python/issues/5814)) +1. Remove extra overlay on editor when matching parentheses. + ([#5815](https://github.com/Microsoft/vscode-python/issues/5815)) +1. Fix theme color missing errors inside interactive window. + ([#5827](https://github.com/Microsoft/vscode-python/issues/5827)) +1. Fix problem with shift+enter not working after using goto source. + ([#5829](https://github.com/Microsoft/vscode-python/issues/5829)) +1. Fix CI failures related to history import changes. + ([#5844](https://github.com/Microsoft/vscode-python/issues/5844)) +1. Disable quoting of paths sent to the debugger as arguments. + ([#5861](https://github.com/Microsoft/vscode-python/issues/5861)) +1. Fix shift+enter to work in newly created files with cells. + ([#5879](https://github.com/Microsoft/vscode-python/issues/5879)) +1. Fix nightly failures caused by new jupyter command line. + ([#5883](https://github.com/Microsoft/vscode-python/issues/5883)) +1. Improve accessibility of the 'Python Interactive' window. + ([#5884](https://github.com/Microsoft/vscode-python/issues/5884)) +1. Auto preview notebooks on import. + ([#5891](https://github.com/Microsoft/vscode-python/issues/5891)) +1. Fix liveloss test to not have so many dependencies. + ([#5909](https://github.com/Microsoft/vscode-python/issues/5909)) +1. Fixes to detection of the shell. + ([#5916](https://github.com/Microsoft/vscode-python/issues/5916)) +1. Fixes to activation of Conda environments. + ([#5929](https://github.com/Microsoft/vscode-python/issues/5929)) +1. Fix themes in the interactive window that use 3 color hex values (like Cobalt2). + ([#5950](https://github.com/Microsoft/vscode-python/issues/5950)) +1. Fix jupyter services node-fetch connection issue. + ([#5956](https://github.com/Microsoft/vscode-python/issues/5956)) +1. Allow selection and running of indented code in the python interactive window. + ([#5983](https://github.com/Microsoft/vscode-python/issues/5983)) +1. Account for files being opened in Visual Studio Code that do not belong to a workspace. + ([#6624](https://github.com/Microsoft/vscode-python/issues/6624)) +1. Accessibility pass on plot viewer + ([#6020](https://github.com/Microsoft/vscode-python/issues/6020)) +1. Allow for both password and self cert server to work together + ([#6265](https://github.com/Microsoft/vscode-python/issues/6265)) +1. Fix pdf export in release bits. + ([#6277](https://github.com/Microsoft/vscode-python/issues/6277)) + +### Code Health + +1. Add code coverage reporting. + ([#4472](https://github.com/Microsoft/vscode-python/issues/4472)) +1. Minimize data sent as part of the `ERROR` telemetry event. + ([#4602](https://github.com/Microsoft/vscode-python/issues/4602)) +1. Fixes to decorator tests. + ([#5085](https://github.com/Microsoft/vscode-python/issues/5085)) +1. Add sorting test for DataViewer. + ([#5415](https://github.com/Microsoft/vscode-python/issues/5415)) +1. Rename "unit test" to "tests" from drop menu when clicking on "Run Tests" on the status bar. + ([#5605](https://github.com/Microsoft/vscode-python/issues/5605)) +1. Added telemetry to track memory usage of the `Jedi Language Server` process. + ([#5726](https://github.com/Microsoft/vscode-python/issues/5726)) +1. Fix nightly functional tests from timing out during process cleanup. + ([#5870](https://github.com/Microsoft/vscode-python/issues/5870)) +1. Change how telemetry is sent for the 'shift+enter' banner. + ([#5887](https://github.com/Microsoft/vscode-python/issues/5887)) +1. Fixes to gulp script used to bundle the extension with `WebPack`. + ([#5932](https://github.com/Microsoft/vscode-python/issues/5932)) +1. Tighten up the import-matching regex to minimize false-positives. + ([#5988](https://github.com/Microsoft/vscode-python/issues/5988)) +1. Merge multiple coverage reports into one. + ([#6000](https://github.com/Microsoft/vscode-python/issues/6000)) +1. Fix DataScience nightly tests. + ([#6032](https://github.com/Microsoft/vscode-python/issues/6032)) +1. Update version of TypeScript to 3.5. + ([#6033](https://github.com/Microsoft/vscode-python/issues/6033)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.20](https://pypi.org/project/isort/4.3.20/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.5.18875 (6 June 2019) + +### Fixes + +1. Disable quoting of paths sent to the debugger as arguments. + ([#5861](https://github.com/microsoft/vscode-python/issues/5861)) +1. Fixes to activation of Conda environments. + ([#5929](https://github.com/microsoft/vscode-python/issues/5929)) + +## 2019.5.18678 (5 June 2019) + +### Fixes + +1. Fixes to detection of the shell. + ([#5916](https://github.com/microsoft/vscode-python/issues/5916)) + +## 2019.5.18875 (6 June 2019) + +### Fixes + +1. Disable quoting of paths sent to the debugger as arguments. + ([#5861](https://github.com/microsoft/vscode-python/issues/5861)) +1. Fixes to activation of Conda environments. + ([#5929](https://github.com/microsoft/vscode-python/issues/5929)) + +## 2019.5.18678 (5 June 2019) + +### Fixes + +1. Fixes to detection of the shell. + ([#5916](https://github.com/microsoft/vscode-python/issues/5916)) + +## 2019.5.18426 (4 June 2019) + +### Fixes + +1. Changes to identification of `shell` for the activation of environments in the terminal. + ([#5743](https://github.com/microsoft/vscode-python/issues/5743)) + +## 2019.5.17517 (30 May 2019) + +### Fixes + +1. Revert changes related to pathMappings in `launch.json` for `debugging` [#3568](https://github.com/Microsoft/vscode-python/issues/3568) + ([#5833](https://github.com/microsoft/vscode-python/issues/5833)) + +## 2019.5.17059 (28 May 2019) + +### Enhancements + +1. Add visual separation between the variable explorer and the rest of the Interactive Window content + ([#5389](https://github.com/Microsoft/vscode-python/issues/5389)) +1. Show a message when no variables are defined + ([#5228](https://github.com/Microsoft/vscode-python/issues/5228)) +1. Variable explorer UI fixes via PM / designer + ([#5274](https://github.com/Microsoft/vscode-python/issues/5274)) +1. Allow column sorting in variable explorer + ([#5281](https://github.com/Microsoft/vscode-python/issues/5281)) +1. Provide basic intellisense in Interactive Windows, using the language server. + ([#5342](https://github.com/Microsoft/vscode-python/issues/5342)) +1. Add support for Jupyter autocomplete data in Interactive Window. + ([#5346](https://github.com/Microsoft/vscode-python/issues/5346)) +1. Swap getsizeof size value for something more sensible in the variable explorer + ([#5368](https://github.com/Microsoft/vscode-python/issues/5368)) +1. Pass parent debug session to child debug sessions using new DA API + ([#5464](https://github.com/Microsoft/vscode-python/issues/5464)) + +### Fixes + +1. Advance to the next cell if cursor is in the current cell and user clicks 'Run Cell' + ([#5067](https://github.com/Microsoft/vscode-python/issues/5067)) +1. Fix import/export paths to be escaped on windows. + ([#5386](https://github.com/Microsoft/vscode-python/issues/5386)) +1. Fix magics running from a python file. + ([#5537](https://github.com/Microsoft/vscode-python/issues/5537)) +1. Change scrolling to not animate to workaround async updates breaking the animation. + ([#5560](https://github.com/Microsoft/vscode-python/issues/5560)) +1. Add support for opening hyperlinks from the interactive window. + ([#5630](https://github.com/Microsoft/vscode-python/issues/5630)) +1. Add 'Add empty cell to file' command. Shortcut for having to type '#%%' + ([#5667](https://github.com/Microsoft/vscode-python/issues/5667)) +1. Add 'ctrl+enter' as a keyboard shortcut for run current cell (runs without advancing) + ([#5673](https://github.com/Microsoft/vscode-python/issues/5673)) +1. Adjust input box prompt to look more an IPython console prompt. + ([#5729](https://github.com/Microsoft/vscode-python/issues/5729)) +1. Fix horizontal scrolling in the Interactive Window + ([#5734](https://github.com/Microsoft/vscode-python/issues/5734)) +1. Fix problem with using up/down arrows in autocomplete. + ([#5774](https://github.com/Microsoft/vscode-python/issues/5774)) +1. Fix latex and markdown scrolling. + ([#5775](https://github.com/Microsoft/vscode-python/issues/5775)) +1. Use the correct activation script for conda environments + ([#4402](https://github.com/Microsoft/vscode-python/issues/4402)) +1. Improve pipenv error messages (thanks [David Lechner](https://github.com/dlech)) + ([#4866](https://github.com/Microsoft/vscode-python/issues/4866)) +1. Quote paths returned by debugger API + ([#4966](https://github.com/Microsoft/vscode-python/issues/4966)) +1. Reliably end test tasks in Azure Pipelines. + ([#5129](https://github.com/Microsoft/vscode-python/issues/5129)) +1. Append `--pre` to black installation command so pipenv can properly resolve it. + (thanks [Erin O'Connell](https://github.com/erinxocon)) + ([#5171](https://github.com/Microsoft/vscode-python/issues/5171)) +1. Make background cell color useable in all themes. + ([#5236](https://github.com/Microsoft/vscode-python/issues/5236)) +1. Filtered rows shows 'fetching' instead of No rows. + ([#5278](https://github.com/Microsoft/vscode-python/issues/5278)) +1. Always show pytest's output when it fails. + ([#5313](https://github.com/Microsoft/vscode-python/issues/5313)) +1. Value 'None' sometimes shows up in the Count column of the variable explorer + ([#5387](https://github.com/Microsoft/vscode-python/issues/5387)) +1. Multi-dimensional arrays don't open in the data viewer. + ([#5395](https://github.com/Microsoft/vscode-python/issues/5395)) +1. Fix sorting of lists with numbers and missing entries. + ([#5414](https://github.com/Microsoft/vscode-python/issues/5414)) +1. Fix error with bad len() values in variable explorer + ([#5420](https://github.com/Microsoft/vscode-python/issues/5420)) +1. Remove trailing commas from JSON files. + (thanks [Romain](https://github.com/quarthex)) + ([#5437](https://github.com/Microsoft/vscode-python/issues/5437)) +1. Handle missing index columns and non trivial data types for columns. + ([#5452](https://github.com/Microsoft/vscode-python/issues/5452)) +1. Fix ignoreVscodeTheme to play along with dynamic theme updates. Also support setting in the variable explorer. + ([#5480](https://github.com/Microsoft/vscode-python/issues/5480)) +1. Fix matplotlib updating for dark theme after restarting + ([#5486](https://github.com/Microsoft/vscode-python/issues/5486)) +1. Add dev flag to poetry installer. + (thanks [Yan Pashkovsky](https://github.com/Yanpas)) + ([#5496](https://github.com/Microsoft/vscode-python/issues/5496)) +1. Default `PYTHONPATH` to an empty string if the environment variable is not defined. + ([#5579](https://github.com/Microsoft/vscode-python/issues/5579)) +1. Fix problems if other language kernels are installed that are using python under the covers (bash is one such example). + ([#5586](https://github.com/Microsoft/vscode-python/issues/5586)) +1. Allow collapsed code to affect intellisense. + ([#5631](https://github.com/Microsoft/vscode-python/issues/5631)) +1. Eliminate search support in the mini-editors in the Python Interactive window. + ([#5637](https://github.com/Microsoft/vscode-python/issues/5637)) +1. Fix perf problem with intellisense in the Interactive Window. + ([#5697](https://github.com/Microsoft/vscode-python/issues/5697)) +1. Using "request": "launch" item in launch.json for debugging sends pathMappings + ([#3568](https://github.com/Microsoft/vscode-python/issues/3568)) +1. Fix perf issues with long collections and variable explorer + ([#5511](https://github.com/Microsoft/vscode-python/issues/5511)) +1. Changed synchronous file system operation into async + ([#4895](https://github.com/Microsoft/vscode-python/issues/4895)) +1. Update ptvsd to [4.2.10](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.10). + - No longer switch off getpass on import. + - Fixes a crash on evaluate request. + - Fix a issue with running no-debug. + - Fixes issue with forwarding sys.stdin.read(). + - Remove sys.prefix form library roots. + +### Code Health + +1. Deprecate [travis](https://travis-ci.org/) in favor of [Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/). + ([#4024](https://github.com/Microsoft/vscode-python/issues/4024)) +1. Smoke Tests must be run on nightly and CI on Azdo + ([#5090](https://github.com/Microsoft/vscode-python/issues/5090)) +1. Increase timeout and retries in Jupyter wait for idle + ([#5430](https://github.com/Microsoft/vscode-python/issues/5430)) +1. Update manual test plan for Variable Explorer and Data Viewer + ([#5476](https://github.com/Microsoft/vscode-python/issues/5476)) +1. Auto-update version number in `CHANGELOG.md` in the CI pipeline. + ([#5523](https://github.com/Microsoft/vscode-python/issues/5523)) +1. Fix security issues. + ([#5538](https://github.com/Microsoft/vscode-python/issues/5538)) +1. Send logging output into a text file on CI server. + ([#5651](https://github.com/Microsoft/vscode-python/issues/5651)) +1. Fix python 2.7 and 3.5 variable explorer nightly tests + ([#5433](https://github.com/Microsoft/vscode-python/issues/5433)) +1. Update isort to version 4.3.20. + (Thanks [Andrew Blakey](https://github.com/ablakey)) + ([#5642](https://github.com/Microsoft/vscode-python/issues/5642)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.20](https://pypi.org/project/isort/4.3.20/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.4.1 (24 April 2019) + +### Fixes + +1. Remove trailing commas in JSON files. + (thanks [Romain](https://github.com/quarthex)) + ([#5437](https://github.com/Microsoft/vscode-python/issues/5437)) + +## 2019.4.0 (23 April 2019) + +### Enhancements + +1. Download the language server using HTTP if `http.proxyStrictSSL` is set to `true`. + ([#2849](https://github.com/Microsoft/vscode-python/issues/2849)) +1. Launch the `Python` debug configuration UI when manually adding entries into the `launch.json` file. + ([#3321](https://github.com/Microsoft/vscode-python/issues/3321)) +1. Add tracking of 'current' cell in the editor. Also add cell boundaries for non active cell. + ([#3542](https://github.com/Microsoft/vscode-python/issues/3542)) +1. Change default behavior of debugger to display return values. + ([#3754](https://github.com/Microsoft/vscode-python/issues/3754)) +1. Replace setting `debugStdLib` with `justMyCode` + ([#4032](https://github.com/Microsoft/vscode-python/issues/4032)) +1. Change "Unit Test" phrasing to "Test" or "Testing". + ([#4384](https://github.com/Microsoft/vscode-python/issues/4384)) +1. Auto expand tree view in `Test Explorer` to display failed tests. + ([#4386](https://github.com/Microsoft/vscode-python/issues/4386)) +1. Add a data grid control and web view panel. + ([#4675](https://github.com/Microsoft/vscode-python/issues/4675)) +1. Add support for viewing dataframes, lists, dicts, nparrays. + ([#4677](https://github.com/Microsoft/vscode-python/issues/4677)) +1. Auto-expand the first level of the test explorer tree view. + ([#4767](https://github.com/Microsoft/vscode-python/issues/4767)) +1. Use `Python` code for discovery of tests when using `pytest`. + ([#4795](https://github.com/Microsoft/vscode-python/issues/4795)) +1. Intergrate the variable explorer into the header better and refactor HTML and CSS. + ([#4800](https://github.com/Microsoft/vscode-python/issues/4800)) +1. Integrate the variable viewer with the IJupyterVariable interface. + ([#4802](https://github.com/Microsoft/vscode-python/issues/4802)) +1. Include number of skipped tests in Test Data item tooltip. + ([#4849](https://github.com/Microsoft/vscode-python/issues/4849)) +1. Add prompt to select virtual environment for the worskpace. + ([#4908](https://github.com/Microsoft/vscode-python/issues/4908)) +1. Prompt to turn on Pylint if a `pylintrc` or `.pylintrc` file is found. + ([#4941](https://github.com/Microsoft/vscode-python/issues/4941)) +1. Variable explorer handles new cell submissions. + ([#4948](https://github.com/Microsoft/vscode-python/issues/4948)) +1. Pass one at getting our data grid styled correctly to match vscode styles and the spec. + ([#4998](https://github.com/Microsoft/vscode-python/issues/4998)) +1. Ensure `Language Server` can start without [ICU](http://site.icu-project.org/home). + ([#5043](https://github.com/Microsoft/vscode-python/issues/5043)) +1. Support running under docker. + ([#5047](https://github.com/Microsoft/vscode-python/issues/5047)) +1. Add exclude list to variable viewer. + ([#5104](https://github.com/Microsoft/vscode-python/issues/5104)) +1. Display a tip to the user informing them of the ability to change the interpreter from the statusbar. + ([#5180](https://github.com/Microsoft/vscode-python/issues/5180)) +1. Hook up the variable explorer to the data frame explorer. + ([#5187](https://github.com/Microsoft/vscode-python/issues/5187)) +1. Remove the debug config snippets (rely on handler instead). + ([#5189](https://github.com/Microsoft/vscode-python/issues/5189)) +1. Add setting to just enable/disable the data science codelens. + ([#5211](https://github.com/Microsoft/vscode-python/issues/5211)) +1. Change settings from `python.unitTest.*` to `python.testing.*`. + ([#5219](https://github.com/Microsoft/vscode-python/issues/5219)) +1. Add telemetry for variable explorer and turn on by default. + ([#5337](https://github.com/Microsoft/vscode-python/issues/5337)) +1. Show a message when no variables are defined + ([#5228](https://github.com/Microsoft/vscode-python/issues/5228)) +1. Variable explorer UI fixes via PM / designer + ([#5274](https://github.com/Microsoft/vscode-python/issues/5274)) +1. Allow column sorting in variable explorer + ([#5281](https://github.com/Microsoft/vscode-python/issues/5281)) +1. Swap getsizeof size value for something more sensible in the variable explorer + ([#5368](https://github.com/Microsoft/vscode-python/issues/5368)) + +### Fixes + +1. Ignore the extension's Python files when debugging. + ([#3201](https://github.com/Microsoft/vscode-python/issues/3201)) +1. Dispose processes started within the extension during. + ([#3331](https://github.com/Microsoft/vscode-python/issues/3331)) +1. Fix problem with errors not showing up for import when no jupyter installed. + ([#3958](https://github.com/Microsoft/vscode-python/issues/3958)) +1. Fix tabs in comments to come out in cells. + ([#4029](https://github.com/Microsoft/vscode-python/issues/4029)) +1. Use configuration API and provide Resource when retrieving settings. + ([#4486](https://github.com/Microsoft/vscode-python/issues/4486)) +1. When debugging, the extension correctly uses custom `.env` files. + ([#4537](https://github.com/Microsoft/vscode-python/issues/4537)) +1. Accomadate trailing commands in the JSON contents of `launch.json` file. + ([#4543](https://github.com/Microsoft/vscode-python/issues/4543)) +1. Kill liveshare sessions if a guest connects without the python extension installed. + ([#4947](https://github.com/Microsoft/vscode-python/issues/4947)) +1. Shutting down a session should not cause the host to stop working. + ([#4949](https://github.com/Microsoft/vscode-python/issues/4949)) +1. Fix cell spacing issues. + ([#4979](https://github.com/Microsoft/vscode-python/issues/4979)) +1. Fix hangs in functional tests. + ([#4992](https://github.com/Microsoft/vscode-python/issues/4992)) +1. Fix triple quoted comments in cells to not affect anything. + ([#5012](https://github.com/Microsoft/vscode-python/issues/5012)) +1. Restarting the kernel will eventually force Jupyter server to shutdown if it doesn't come back. + ([#5025](https://github.com/Microsoft/vscode-python/issues/5025)) +1. Adjust styling for data viewer. + ([#5058](https://github.com/Microsoft/vscode-python/issues/5058)) +1. Fix MimeTypes test after we stopped stripping comments. + ([#5086](https://github.com/Microsoft/vscode-python/issues/5086)) +1. No prompt displayed to install pylint. + ([#5087](https://github.com/Microsoft/vscode-python/issues/5087)) +1. Fix scrolling in the interactive window. + ([#5131](https://github.com/Microsoft/vscode-python/issues/5131)) +1. Default colors when theme.json cannot be found. + Fix Python interactive window to update when theme changes. + ([#5136](https://github.com/Microsoft/vscode-python/issues/5136)) +1. Replace 'Run Above' and 'Run Below' in the palette with 'Run Cells Above Cursor' and 'Run Current Cell and Below'. + ([#5143](https://github.com/Microsoft/vscode-python/issues/5143)) +1. Variables not cleared after a kernel restart. + ([#5244](https://github.com/Microsoft/vscode-python/issues/5244)) +1. Fix variable explorer to work in Live Share. + ([#5277](https://github.com/Microsoft/vscode-python/issues/5277)) +1. Update matplotlib based on theme changes. + ([#5294](https://github.com/Microsoft/vscode-python/issues/5294)) +1. Restrict files from being processed by `Language Server` only when in a mult-root workspace. + ([#5333](https://github.com/Microsoft/vscode-python/issues/5333)) +1. Fix dataviewer header column alignment. + ([#5351](https://github.com/Microsoft/vscode-python/issues/5351)) +1. Make background cell color useable in all themes. + ([#5236](https://github.com/Microsoft/vscode-python/issues/5236)) +1. Filtered rows shows 'fetching' instead of No rows. + ([#5278](https://github.com/Microsoft/vscode-python/issues/5278)) +1. Multi-dimensional arrays don't open in the data viewer. + ([#5395](https://github.com/Microsoft/vscode-python/issues/5395)) +1. Fix sorting of lists with numbers and missing entries. + ([#5414](https://github.com/Microsoft/vscode-python/issues/5414)) +1. Fix error with bad len() values in variable explorer + ([#5420](https://github.com/Microsoft/vscode-python/issues/5420)) +1. Update ptvsd to [4.2.8](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.8). + - Path mapping bug fixes. + - Fix for hang when using debug console. + - Fix for set next statement. + - Fix for multi-threading. + +### Code Health + +1. Enable typescript's strict mode. + ([#611](https://github.com/Microsoft/vscode-python/issues/611)) +1. Update to use `Node` version `10.5.0`. + ([#1138](https://github.com/Microsoft/vscode-python/issues/1138)) +1. Update `launch.json` to use `internalConsole` instead of none. + ([#4321](https://github.com/Microsoft/vscode-python/issues/4321)) +1. Change flaky tests (relying on File System Watchers) into unit tests. + ([#4468](https://github.com/Microsoft/vscode-python/issues/4468)) +1. Corrected Smoke test failure for 'Run Python File In Terminal'. + ([#4515](https://github.com/Microsoft/vscode-python/issues/4515)) +1. Drop (official) support for Python 3.4. + ([#4528](https://github.com/Microsoft/vscode-python/issues/4528)) +1. Support debouncing decorated async methods. + ([#4641](https://github.com/Microsoft/vscode-python/issues/4641)) +1. Add functional tests for pytest adapter script. + ([#4739](https://github.com/Microsoft/vscode-python/issues/4739)) +1. Remove the use of timers in unittest code. Simulate the passing of time instead. + ([#4776](https://github.com/Microsoft/vscode-python/issues/4776)) +1. Add functional testing for variable explorer. + ([#4803](https://github.com/Microsoft/vscode-python/issues/4803)) +1. Add tests for variable explorer Python files. + ([#4804](https://github.com/Microsoft/vscode-python/issues/4804)) +1. Add real environment variables provider on to get functional tests to pass on macOS. + ([#4820](https://github.com/Microsoft/vscode-python/issues/4820)) +1. Handle done on all jupyter requests to make sure an unhandle exception isn't passed on shutdown. + ([#4827](https://github.com/Microsoft/vscode-python/issues/4827)) +1. Clean up language server initialization and configuration. + ([#4832](https://github.com/Microsoft/vscode-python/issues/4832)) +1. Hash imports of top-level packages to see what users need supported. + ([#4852](https://github.com/Microsoft/vscode-python/issues/4852)) +1. Have `tpn` clearly state why a project's license entry in the configuration file is considered stale. + ([#4865](https://github.com/Microsoft/vscode-python/issues/4865)) +1. Kill the test process on CI, 10s after the tests have completed. + ([#4905](https://github.com/Microsoft/vscode-python/issues/4905)) +1. Remove hardcoded Azdo Pipeline of 30m, leaving it to the default of 60m. + ([#4914](https://github.com/Microsoft/vscode-python/issues/4914)) +1. Use the `Python` interpreter prescribed by CI instead of trying to locate the best possible one. + ([#4920](https://github.com/Microsoft/vscode-python/issues/4920)) +1. Skip linter tests correctly. + ([#4923](https://github.com/Microsoft/vscode-python/issues/4923)) +1. Remove redundant compilation step on CI. + ([#4926](https://github.com/Microsoft/vscode-python/issues/4926)) +1. Dispose handles to timers created from using `setTimeout`. + ([#4930](https://github.com/Microsoft/vscode-python/issues/4930)) +1. Ensure sockets get disposed along with other resources. + ([#4935](https://github.com/Microsoft/vscode-python/issues/4935)) +1. Fix intermittent test failure with listeners. + ([#4936](https://github.com/Microsoft/vscode-python/issues/4936)) +1. Update `mocha` to the latest version. + ([#4937](https://github.com/Microsoft/vscode-python/issues/4937)) +1. Remove redundant mult-root tests. + ([#4943](https://github.com/Microsoft/vscode-python/issues/4943)) +1. Fix intermittent test failure with kernel shutdown. + ([#4951](https://github.com/Microsoft/vscode-python/issues/4951)) +1. Update version of [isort](https://pypi.org/project/isort/) to `4.3.17` + ([#5059](https://github.com/Microsoft/vscode-python/issues/5059)) +1. Fix typo and use constants instead of hardcoded command names. + (thanks [Allan Wang](https://github.com/AllanWang)) + ([#5204](https://github.com/Microsoft/vscode-python/issues/5204)) +1. Add datascience specific settings to telemetry gathered. Make sure to scrape any strings of PII. + ([#5212](https://github.com/Microsoft/vscode-python/issues/5212)) +1. Add telemetry around people hitting 'no' on the enable interactive shift enter. + Reword the message to be more descriptive. + ([#5213](https://github.com/Microsoft/vscode-python/issues/5213)) +1. Fix failing variable explorer test. + ([#5348](https://github.com/Microsoft/vscode-python/issues/5348)) +1. Reliably end test tasks in Azure Pipelines. + ([#5129](https://github.com/Microsoft/vscode-python/issues/5129)) +1. Deprecate [travis](https://travis-ci.org/) in favor of [Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/). + ([#4024](https://github.com/Microsoft/vscode-python/issues/4024)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.3.3 (8 April 2019) + +### Fixes + +1. Update ptvsd to [4.2.7](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.7). + - Fix issues related to debugging Django templates. +1. Update the Python language server to 0.2.47. + +### Code Health + +1. Capture telemetry to track switching to and from the Language Server. + ([#5162](https://github.com/Microsoft/vscode-python/issues/5162)) + +## 2019.3.2 (2 April 2019) + +### Fixes + +1. Fix regression preventing the expansion of variables in the watch window and the debug console. + ([#5035](https://github.com/Microsoft/vscode-python/issues/5035)) +1. Display survey banner (again) for Language Server when using current Language Server. + ([#5064](https://github.com/Microsoft/vscode-python/issues/5064)) +1. Update ptvsd to [4.2.6](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.6). + ([#5083](https://github.com/Microsoft/vscode-python/issues/5083)) + - Fix issue with expanding variables in watch window and hover. + - Fix issue with launching a sub-module. + +### Code Health + +1. Capture telemetry to track which installer was used when installing packages via the extension. + ([#5063](https://github.com/Microsoft/vscode-python/issues/5063)) + +## 2019.3.1 (28 March 2019) + +### Enhancements + +1. Use the download same logic for `stable` channel of the `Language Server` as that in `beta`. + ([#4960](https://github.com/Microsoft/vscode-python/issues/4960)) + +### Code Health + +1. Capture telemetry when tests are disabled.. + ([#4801](https://github.com/Microsoft/vscode-python/issues/4801)) + +## 2019.3.6139 (26 March 2019) + +### Enhancements + +1. Add support for poetry to install packages. + ([#1871](https://github.com/Microsoft/vscode-python/issues/1871)) +1. Disabled opening the output pane when sorting imports via isort fails. + (thanks [chrised](https://github.com/chrised/)) + ([#2522](https://github.com/Microsoft/vscode-python/issues/2522)) +1. Remove run all cells codelens and replace with run above and run below commands + Add run to and from line commands in context menu + ([#4259](https://github.com/Microsoft/vscode-python/issues/4259)) +1. Support multi-root workspaces in test explorer. + ([#4268](https://github.com/Microsoft/vscode-python/issues/4268)) +1. Added support for fetching variable values from the jupyter server + ([#4291](https://github.com/Microsoft/vscode-python/issues/4291)) +1. Added commands translation for polish locale. + (thanks [pypros](https://github.com/pypros/)) + ([#4435](https://github.com/Microsoft/vscode-python/issues/4435)) +1. Show sub-tests in a subtree in the test explorer. + ([#4503](https://github.com/Microsoft/vscode-python/issues/4503)) +1. Add support for palette commands for Live Share scenarios. + ([#4520](https://github.com/Microsoft/vscode-python/issues/4520)) +1. Retain state of tests when auto discovering tests. + ([#4576](https://github.com/Microsoft/vscode-python/issues/4576)) +1. Update icons and tooltip in test explorer indicating status of test files/suites + ([#4583](https://github.com/Microsoft/vscode-python/issues/4583)) +1. Add 'ignoreVscodeTheme' setting to allow a user to skip using the theme for VS Code in the Python Interactive Window. + ([#4640](https://github.com/Microsoft/vscode-python/issues/4640)) +1. Add telemetry around imports. + ([#4718](https://github.com/Microsoft/vscode-python/issues/4718)) +1. Update status of test suite when all tests pass + ([#4727](https://github.com/Microsoft/vscode-python/issues/4727)) +1. Add button to ignore the message warning about the use of the macOS system install of Python. + (thanks [Alina Lobastova](https://github.com/alina7091)) + ([#4448](https://github.com/Microsoft/vscode-python/issues/4448)) +1. Add "Run In Interactive" command to run the contents of a file not cell by cell. Group data science context commands in one group. Add run file command to explorer context menu. + ([#4855](https://github.com/Microsoft/vscode-python/issues/4855)) + +### Fixes + +1. Add 'errorBackgroundColor' (defaults to white/#FFFFFF) for errors in the Interactive Window. Computes foreground based on background. + ([#3175](https://github.com/Microsoft/vscode-python/issues/3175)) +1. If selection is being sent to the Interactive Windows still allow for context menu commands to run selection in terminal or run file in terminal + ([#4207](https://github.com/Microsoft/vscode-python/issues/4207)) +1. Support multiline comments for markdown cells + ([#4215](https://github.com/Microsoft/vscode-python/issues/4215)) +1. Conda activation fails when there is a space in the env name + ([#4243](https://github.com/Microsoft/vscode-python/issues/4243)) +1. Fixes to ensure tests work in multi-root workspaces. + ([#4268](https://github.com/Microsoft/vscode-python/issues/4268)) +1. Allow Interactive Window to run commands as both `-m jupyter command` and as `-m command` + ([#4306](https://github.com/Microsoft/vscode-python/issues/4306)) +1. Fix shift enter to send selection when cells are defined. + ([#4413](https://github.com/Microsoft/vscode-python/issues/4413)) +1. Test explorer icon should be hidden when tests are disabled + ([#4494](https://github.com/Microsoft/vscode-python/issues/4494)) +1. Fix double running of cells with the context menu + ([#4532](https://github.com/Microsoft/vscode-python/issues/4532)) +1. Show an "unknown" icon when test status is unknown. + ([#4578](https://github.com/Microsoft/vscode-python/issues/4578)) +1. Add sys info when switching interpreters + ([#4588](https://github.com/Microsoft/vscode-python/issues/4588)) +1. Display test explorer when discovery has been run. + ([#4590](https://github.com/Microsoft/vscode-python/issues/4590)) +1. Resolve `pythonPath` before comparing it to shebang + ([#4601](https://github.com/Microsoft/vscode-python/issues/4601)) +1. When sending selection to the Interactive Window nothing selected should send the entire line + ([#4604](https://github.com/Microsoft/vscode-python/issues/4604)) +1. Provide telemetry for when we show the shift+enter banner and if the user clicks yes + ([#4636](https://github.com/Microsoft/vscode-python/issues/4636)) +1. Better error message when connecting to remote server + ([#4666](https://github.com/Microsoft/vscode-python/issues/4666)) +1. Fix problem with restart never finishing + ([#4691](https://github.com/Microsoft/vscode-python/issues/4691)) +1. Fixes to ensure we invoke the right command when running a parameterized test function. + ([#4713](https://github.com/Microsoft/vscode-python/issues/4713)) +1. Handle view state changes for the Python Interactive window so that it gains focus when appropriate. (CTRL+1/2/3 etc should give focus to the interactive window) + ([#4733](https://github.com/Microsoft/vscode-python/issues/4733)) +1. Don't have "run all above" on first cell and don't start history for empty code runs + ([#4743](https://github.com/Microsoft/vscode-python/issues/4743)) +1. Perform case insensitive comparison of Python Environment paths + ([#4797](https://github.com/Microsoft/vscode-python/issues/4797)) +1. Ensure `Jedi` uses the currently selected interpreter. + (thanks [Selim Belhaouane](https://github.com/selimb)) + ([#4687](https://github.com/Microsoft/vscode-python/issues/4687)) +1. Multiline comments with text on the first line break Python Interactive window execution. + ([#4791](https://github.com/Microsoft/vscode-python/issues/4791)) +1. Fix status bar when using Live Share or just starting the Python Interactive window. + ([#4853](https://github.com/Microsoft/vscode-python/issues/4853)) +1. Change the names of our "Run All Cells Above" and "Run Cell and All Below" commands to be more concise + ([#4876](https://github.com/Microsoft/vscode-python/issues/4876)) +1. Ensure the `Python` output panel does not steal focus when there errors in the `Language Server`. + ([#4868](https://github.com/Microsoft/vscode-python/issues/4868)) +1. Update ptvsd to [4.2.5](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.5). + ([#4932](https://github.com/Microsoft/vscode-python/issues/4932)) + - Fix issues with django and jinja2 exceptions. + - Detaching sometimes throws ValueError. + - StackTrace request respecting just-my-code. + - Don't give error redirecting output with pythonw. + - Fix for stop on entry issue. +1. Update the Python language server to 0.2.31. + +### Code Health + +1. Add a Python script to run PyTest correctly for discovery. + ([#4033](https://github.com/Microsoft/vscode-python/issues/4033)) +1. Ensure post npm install scripts do not fail when run more than once. + ([#4109](https://github.com/Microsoft/vscode-python/issues/4109)) +1. Improve Azure DevOps pipeline for PR validation. Added speed improvements, documented the process better, and simplified what happens in PR validation. + ([#4123](https://github.com/Microsoft/vscode-python/issues/4123)) +1. Move to new Azure DevOps instance and bring the Nightly CI build closer to running cleanly by skipping tests and improving reporting transparency. + ([#4336](https://github.com/Microsoft/vscode-python/issues/4336)) +1. Add more logging to diagnose issues getting the Python Interactive window to show up. + Add checks for Conda activation never finishing. + ([#4424](https://github.com/Microsoft/vscode-python/issues/4424)) +1. Update `nyc` and remove `gulp-watch` and `gulp-debounced-watch`. + ([#4490](https://github.com/Microsoft/vscode-python/issues/4490)) +1. Force WS to at least 3.3.1 to alleviate security concerns. + ([#4497](https://github.com/Microsoft/vscode-python/issues/4497)) +1. Add tests for Live Share support. + ([#4521](https://github.com/Microsoft/vscode-python/issues/4521)) +1. Fix running Live Share support in a release build. + ([#4529](https://github.com/Microsoft/vscode-python/issues/4529)) +1. Delete the `pvsc-dev-ext.py` file as it was not being properly maintained. + ([#4530](https://github.com/Microsoft/vscode-python/issues/4530)) +1. Increase timeouts for loading of extension when preparing to run tests. + ([#4540](https://github.com/Microsoft/vscode-python/issues/4540)) +1. Exclude files `travis*.log`, `pythonFiles/tests/**`, `types/**` from the extension. + ([#4554](https://github.com/Microsoft/vscode-python/issues/4554)) +1. Exclude `*.vsix` from source control. + ([#4556](https://github.com/Microsoft/vscode-python/issues/4556)) +1. Add more logging for ECONNREFUSED errors and Jupyter server crashes + ([#4573](https://github.com/Microsoft/vscode-python/issues/4573)) +1. Add travis task to verify bundle can be created. + ([#4711](https://github.com/Microsoft/vscode-python/issues/4711)) +1. Add manual test plan for data science + ([#4716](https://github.com/Microsoft/vscode-python/issues/4716)) +1. Fix Live Share nightly functional tests + ([#4757](https://github.com/Microsoft/vscode-python/issues/4757)) +1. Make cancel test and server cache test more robust + ([#4818](https://github.com/Microsoft/vscode-python/issues/4818)) +1. Generalize code used to parse Test results service + ([#4796](https://github.com/Microsoft/vscode-python/issues/4796)) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +## 2019.2.2 (6 March 2019) + +### Fixes + +1. If selection is being sent to the Interactive Windows still allow for context menu commands to run selection in terminal or run file in terminal + ([#4207](https://github.com/Microsoft/vscode-python/issues/4207)) +1. When sending selection to the Interactive Window nothing selected should send the entire line + ([#4604](https://github.com/Microsoft/vscode-python/issues/4604)) +1. Provide telemetry for when we show the shift-enter banner and if the user clicks yes + ([#4636](https://github.com/Microsoft/vscode-python/issues/4636)) + +## 2019.2.5433 (27 Feb 2019) + +### Fixes + +1. Exclude files `travis*.log`, `pythonFiles/tests/**`, `types/**` from the extension. + ([#4554](https://github.com/Microsoft/vscode-python/issues/4554)) + ([#4566](https://github.com/Microsoft/vscode-python/issues/4566)) + +## 2019.2.0 (26 Feb 2019) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +### Enhancements + +1. Support launch configs for debugging tests. + ([#332](https://github.com/Microsoft/vscode-python/issues/332)) +1. Add way to send code to the Python Interactive window without having to put `#%%` into a file. + ([#3171](https://github.com/Microsoft/vscode-python/issues/3171)) +1. Support simple variable substitution in `.env` files. + ([#3275](https://github.com/Microsoft/vscode-python/issues/3275)) +1. Support live share in Python interactive window (experimental). + ([#3581](https://github.com/Microsoft/vscode-python/issues/3581)) +1. Strip comments before sending so shell command and multiline jupyter magics work correctly. + ([#4064](https://github.com/Microsoft/vscode-python/issues/4064)) +1. Add a build number to our released builds. + ([#4183](https://github.com/Microsoft/vscode-python/issues/4183)) +1. Prompt the user to send shift-enter to the interactive window. + ([#4184](https://github.com/Microsoft/vscode-python/issues/4184)) +1. Added Dutch translation. + (thanks [Robin Martijn](https://github.com/Bowero) with the feedback of [Michael van Tellingen](https://github.com/mvantellingen)) + ([#4186](https://github.com/Microsoft/vscode-python/issues/4186)) +1. Add the Test Activity view. + ([#4272](https://github.com/Microsoft/vscode-python/issues/4272)) +1. Added action buttons to top of Test Explorer. + ([#4275](https://github.com/Microsoft/vscode-python/issues/4275)) +1. Navigation to test output from Test Explorer. + ([#4279](https://github.com/Microsoft/vscode-python/issues/4279)) +1. Add the command 'Configure Unit Tests'. + ([#4286](https://github.com/Microsoft/vscode-python/issues/4286)) +1. Do not update unit test settings if configuration is cancelled. + ([#4287](https://github.com/Microsoft/vscode-python/issues/4287)) +1. Keep testing configuration alive when losing UI focus. + ([#4288](https://github.com/Microsoft/vscode-python/issues/4288)) +1. Display test activity only when tests have been discovered. + ([#4317](https://github.com/Microsoft/vscode-python/issues/4317)) +1. Added a button to configure unit tests when prompting users that tests weren't discovered. + ([#4318](https://github.com/Microsoft/vscode-python/issues/4318)) +1. Use VSC API to open browser window + ([#4322](https://github.com/Microsoft/vscode-python/issues/4322)) +1. Don't shut down the notebook server on window close. + ([#4348](https://github.com/Microsoft/vscode-python/issues/4348)) +1. Added command `Show Output` to display the `Python` output panel. + ([#4362](https://github.com/Microsoft/vscode-python/issues/4362)) +1. Fix order of icons in test explorer and items. + ([#4364](https://github.com/Microsoft/vscode-python/issues/4364)) +1. Run failed tests icon should only appear if and when a test has failed. + ([#4371](https://github.com/Microsoft/vscode-python/issues/4371)) +1. Update ptvsd to [4.2.4](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.4). + ([#4457](https://github.com/Microsoft/vscode-python/issues/4457)) + - Validate breakpoint targets. + - Properly exclude certain files from showing up in the debugger. + +### Fixes + +1. Add support for multi root workspaces with the new language server server. + ([#3008](https://github.com/Microsoft/vscode-python/issues/3008)) +1. Move linting tests to unit-testing for better reliability. + ([#3914](https://github.com/Microsoft/vscode-python/issues/3914)) +1. Allow "Run Cell" code lenses on non-local files. + ([#3995](https://github.com/Microsoft/vscode-python/issues/3995)) +1. Functional test for the input portion of the python interactive window. + ([#4057](https://github.com/Microsoft/vscode-python/issues/4057)) +1. Fix hitting the up arrow on the input prompt for the Python Interactive window to behave like the terminal window when only 1 item in the history. + ([#4145](https://github.com/Microsoft/vscode-python/issues/4145)) +1. Fix problem with webview panel not being dockable anywhere but view column 2. + ([#4237](https://github.com/Microsoft/vscode-python/issues/4237)) +1. More fixes for history in the Python Interactive window input prompt. + ([#4255](https://github.com/Microsoft/vscode-python/issues/4255)) +1. Fix precedence in `parsePyTestModuleCollectionResult`. + (thanks [Tammo Ippen](https://github.com/tammoippen)) + ([#4360](https://github.com/Microsoft/vscode-python/issues/4360)) +1. Revert pipenv activation to not use `pipenv` shell.` + ([#4394](https://github.com/Microsoft/vscode-python/issues/4394)) +1. Fix shift enter to send selection when cells are defined. + ([#4413](https://github.com/Microsoft/vscode-python/issues/4413)) +1. Icons should display only in test explorer. + ([#4418](https://github.com/Microsoft/vscode-python/issues/4418)) +1. Update ptvsd to [4.2.4](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.4). + ([#4457](https://github.com/Microsoft/vscode-python/issues/4457)) + - `BreakOnSystemExitZero` now respected. + - Fix a bug causing breakpoints not to be hit when attached to a remote target. +1. Fix double running of cells with the context menu + ([#4532](https://github.com/Microsoft/vscode-python/issues/4532)) +1. Update the Python language server to 0.1.80. + +### Code Health + +1. Fix all typescript errors when compiled in strict mode. + ([#611](https://github.com/Microsoft/vscode-python/issues/611)) +1. Get functional tests running nightly again. + ([#3973](https://github.com/Microsoft/vscode-python/issues/3973)) +1. Turn on strict type checking (typescript compiling) for Datascience code. + ([#4058](https://github.com/Microsoft/vscode-python/issues/4058)) +1. Turn on strict typescript compile for the data science react code. + ([#4091](https://github.com/Microsoft/vscode-python/issues/4091)) +1. Fix issue causing debugger tests to timeout on CI servers. + ([#4148](https://github.com/Microsoft/vscode-python/issues/4148)) +1. Don't register language server onTelemetry when downloadLanguageServer is false. + ([#4199](https://github.com/Microsoft/vscode-python/issues/4199)) +1. Fixes to smoke tests on CI. + ([#4201](https://github.com/Microsoft/vscode-python/issues/4201)) + +## 2019.1.0 (29 Jan 2019) + +### Thanks + +Thanks to the following projects which we fully rely on to provide some of +our features: + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) + +Also thanks to the various projects we provide integrations with which help +make this extension useful: + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [bandit](https://pypi.org/project/bandit/), + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +And finally thanks to the [Python](https://www.python.org/) development team and +community for creating a fantastic programming language and community to be a +part of! + +### Enhancements + +1. Add the capability to have custom regex's for cell/markdown matching + ([#4065](https://github.com/Microsoft/vscode-python/issues/4065)) +1. Perform all validation checks in the background + ([#3019](https://github.com/Microsoft/vscode-python/issues/3019)) +1. Watermark for Python Interactive input prompt + ([#4111](https://github.com/Microsoft/vscode-python/issues/4111)) +1. Create diagnostics for failed/skipped tests that were run with pytest. + (thanks [Chris NeJame](https://github.com/SalmonMode/)) + ([#120](https://github.com/Microsoft/vscode-python/issues/120)) +1. Add the python.pipenvPath config setting. + ([#978](https://github.com/Microsoft/vscode-python/issues/978)) +1. Add localRoot and remoteRoot defaults for Remote Debugging configuration in `launch.json`. + ([#1385](https://github.com/Microsoft/vscode-python/issues/1385)) +1. Activate `pipenv` environments in the shell using the command `pipenv shell`. + ([#2855](https://github.com/Microsoft/vscode-python/issues/2855)) +1. Use Pylint message names instead of codes + (thanks to [Roman Kornev](https://github.com/RomanKornev/)) + ([#2906](https://github.com/Microsoft/vscode-python/issues/2906)) +1. Add ability to enter Python code directly into the Python Interactive window + ([#3101](https://github.com/Microsoft/vscode-python/issues/3101)) +1. Allow interactive window inputs to either be collapsed by default or totally hidden + ([#3266](https://github.com/Microsoft/vscode-python/issues/3266)) +1. Notify the user when language server extraction fails + ([#3268](https://github.com/Microsoft/vscode-python/issues/3268)) +1. Indent on enter after line continuations. + ([#3284](https://github.com/Microsoft/vscode-python/issues/3284)) +1. Improvements to automatic selection of the Python interpreter. + ([#3369](https://github.com/Microsoft/vscode-python/issues/3369)) +1. Add support for column numbers for problems returned by `mypy`. + (thanks [Eric Traut](https://github.com/erictraut)) + ([#3597](https://github.com/Microsoft/vscode-python/issues/3597)) +1. Display actionable message when language server is not supported + ([#3634](https://github.com/Microsoft/vscode-python/issues/3634)) +1. Make sure we are looking for conda in all the right places + ([#3641](https://github.com/Microsoft/vscode-python/issues/3641)) +1. Improvements to message displayed when linter is not installed + ([#3659](https://github.com/Microsoft/vscode-python/issues/3659)) +1. Improvements to message displayed when Python path is invalid (in launch.json) + ([#3661](https://github.com/Microsoft/vscode-python/issues/3661)) +1. Add the Jupyter Server URI to the Interactive Window info cell + ([#3668](https://github.com/Microsoft/vscode-python/issues/3668)) +1. Handle errors happening during extension activation. + ([#3740](https://github.com/Microsoft/vscode-python/issues/3740)) +1. Validate Mac Interpreters in the background. + ([#3908](https://github.com/Microsoft/vscode-python/issues/3908)) +1. When cell inputs to Python Interactive are hidden, don't show cells without any output + ([#3981](https://github.com/Microsoft/vscode-python/issues/3981)) + +### Fixes + +1. Have the new export commands use our directory change code + ([#4140](https://github.com/Microsoft/vscode-python/issues/4140)) +1. Theme should not be exported without output when doing an export. + ([#4141](https://github.com/Microsoft/vscode-python/issues/4141)) +1. Deleting all cells should not remove the input prompt + ([#4152](https://github.com/Microsoft/vscode-python/issues/4152)) +1. Fix ctrl+c to work in code that has already been entered + ([#4168](https://github.com/Microsoft/vscode-python/issues/4168)) +1. Auto-select virtual environment in multi-root workspaces + ([#3501](https://github.com/Microsoft/vscode-python/issues/3501)) +1. Validate interpreter in multi-root workspaces + ([#3502](https://github.com/Microsoft/vscode-python/issues/3502)) +1. Allow clicking anywhere in an input cell to give focus to the input box for the Python Interactive window + ([#4076](https://github.com/Microsoft/vscode-python/issues/4076)) +1. Cursor in Interactive Windows now appears on whitespace + ([#4081](https://github.com/Microsoft/vscode-python/issues/4081)) +1. Fix problem with double scrollbars when typing in the input window. Make code wrap instead. + ([#4084](https://github.com/Microsoft/vscode-python/issues/4084)) +1. Remove execution count from the prompt cell. + ([#4086](https://github.com/Microsoft/vscode-python/issues/4086)) +1. Make sure showing a plain Python Interactive window lists out the sys info + ([#4088](https://github.com/Microsoft/vscode-python/issues/4088)) +1. Fix Python interactive window up/down arrows in the input prompt to behave like a terminal. + ([#4092](https://github.com/Microsoft/vscode-python/issues/4092)) +1. Handle stdout changes with updates to pytest 4.1.x series (without breaking 4.0.x series parsing). + ([#4099](https://github.com/Microsoft/vscode-python/issues/4099)) +1. Fix bug affecting multiple linters used in a workspace. + (thanks [Ilia Novoselov](https://github.com/nullie)) + ([#2571](https://github.com/Microsoft/vscode-python/issues/2571)) +1. Activate any selected Python Environment when running unit tests. + ([#3330](https://github.com/Microsoft/vscode-python/issues/3330)) +1. Ensure extension does not start multiple language servers. + ([#3346](https://github.com/Microsoft/vscode-python/issues/3346)) +1. Add support for running an entire file in the Python Interactive window + ([#3362](https://github.com/Microsoft/vscode-python/issues/3362)) +1. When in multi-root workspace, store selected python path in the `settings.json` file of the workspace folder. + ([#3419](https://github.com/Microsoft/vscode-python/issues/3419)) +1. Fix console wrapping in output so that console based status bars and spinners work. + ([#3529](https://github.com/Microsoft/vscode-python/issues/3529)) +1. Support other virtual environments besides conda + ([#3537](https://github.com/Microsoft/vscode-python/issues/3537)) +1. Fixed tests related to the `onEnter` format provider. + ([#3674](https://github.com/Microsoft/vscode-python/issues/3674)) +1. Lowering threshold for Language Server support on a platform. + ([#3693](https://github.com/Microsoft/vscode-python/issues/3693)) +1. Survive missing kernelspecs as a default will be created. + ([#3699](https://github.com/Microsoft/vscode-python/issues/3699)) +1. Activate the extension when loading ipynb files + ([#3734](https://github.com/Microsoft/vscode-python/issues/3734)) +1. Don't restart the Jupyter server on any settings change. Also don't throw interpreter changed events on unrelated settings changes. + ([#3749](https://github.com/Microsoft/vscode-python/issues/3749)) +1. Support whitespace (tabs and spaces) in output + ([#3757](https://github.com/Microsoft/vscode-python/issues/3757)) +1. Ensure file names are not captured when sending telemetry for unit tests. + ([#3767](https://github.com/Microsoft/vscode-python/issues/3767)) +1. Address problem with Python Interactive icons not working in insider's build. VS Code is more restrictive on what files can load in a webview. + ([#3775](https://github.com/Microsoft/vscode-python/issues/3775)) +1. Fix output so that it wraps '<' entries in <xmp> to allow html like tags to be output. + ([#3824](https://github.com/Microsoft/vscode-python/issues/3824)) +1. Keep the Jupyter remote server URI input box open so you can copy and paste into it easier + ([#3856](https://github.com/Microsoft/vscode-python/issues/3856)) +1. Changes to how source maps are enabled and disabled in the extension. + ([#3905](https://github.com/Microsoft/vscode-python/issues/3905)) +1. Clean up command names for data science + ([#3925](https://github.com/Microsoft/vscode-python/issues/3925)) +1. Add more data when we get an unknown mime type + ([#3945](https://github.com/Microsoft/vscode-python/issues/3945)) +1. Match dots in ignorePatterns globs; fixes .venv not being ignored + (thanks to [Russell Davis](https://github.com/russelldavis)) + ([#3947](https://github.com/Microsoft/vscode-python/issues/3947)) +1. Remove duplicates from interpreters listed in the interpreter selection list. + ([#3953](https://github.com/Microsoft/vscode-python/issues/3953)) +1. Add telemetry for local versus remote connect + ([#3985](https://github.com/Microsoft/vscode-python/issues/3985)) +1. Add new maxOutputSize setting for text output in the Python Interactive window. -1 means infinite, otherwise the number of pixels. + ([#4010](https://github.com/Microsoft/vscode-python/issues/4010)) +1. fix `pythonPath` typo (thanks [David Lechner](https://github.com/dlech)) + ([#4047](https://github.com/Microsoft/vscode-python/issues/4047)) +1. Fix a type in generated header comment when importing a notebook: `DataSciece` --> `DataScience`. + (thanks [sunt05](https://github.com/sunt05)) + ([#4048](https://github.com/Microsoft/vscode-python/issues/4048)) +1. Allow clicking anywhere in an input cell to give focus to the input box for the Python Interactive window + ([#4076](https://github.com/Microsoft/vscode-python/issues/4076)) +1. Fix problem with double scrollbars when typing in the input window. Make code wrap instead. + ([#4084](https://github.com/Microsoft/vscode-python/issues/4084)) +1. Remove execution count from the prompt cell. + ([#4086](https://github.com/Microsoft/vscode-python/issues/4086)) +1. Make sure showing a plain Python Interactive window lists out the sys info + ([#4088](https://github.com/Microsoft/vscode-python/issues/4088)) + +### Code Health + +1. Fix build issue with code.tsx + ([#4156](https://github.com/Microsoft/vscode-python/issues/4156)) +1. Expose an event to notify changes to settings instead of casting settings to concrete class. + ([#642](https://github.com/Microsoft/vscode-python/issues/642)) +1. Created system test to ensure terminal gets activated with anaconda environment + ([#1521](https://github.com/Microsoft/vscode-python/issues/1521)) +1. Added system tests to ensure terminal gets activated with virtualenv environment + ([#1522](https://github.com/Microsoft/vscode-python/issues/1522)) +1. Added system test to ensure terminal gets activated with pipenv + ([#1523](https://github.com/Microsoft/vscode-python/issues/1523)) +1. Fix flaky tests related to auto selection of virtual environments. + ([#2339](https://github.com/Microsoft/vscode-python/issues/2339)) +1. Use enums for event names instead of constants. + ([#2904](https://github.com/Microsoft/vscode-python/issues/2904)) +1. Add tests for clicking buttons in history pane + ([#3084](https://github.com/Microsoft/vscode-python/issues/3084)) +1. Add tests for clear and delete buttons in the history pane + ([#3087](https://github.com/Microsoft/vscode-python/issues/3087)) +1. Add tests for clicking buttons on individual cells + ([#3092](https://github.com/Microsoft/vscode-python/issues/3092)) +1. Handle a 404 when trying to download the language server + ([#3267](https://github.com/Microsoft/vscode-python/issues/3267)) +1. Ensure new warnings are not ignored when bundling the extension with WebPack. + ([#3468](https://github.com/Microsoft/vscode-python/issues/3468)) +1. Update our CI/nightly full build to a YAML definition build in Azure DevOps. + ([#3555](https://github.com/Microsoft/vscode-python/issues/3555)) +1. Add mock of Jupyter API to allow functional tests to run more quickly and more consistently. + ([#3556](https://github.com/Microsoft/vscode-python/issues/3556)) +1. Use Jedi if Language Server fails to activate + ([#3633](https://github.com/Microsoft/vscode-python/issues/3633)) +1. Fix the timeout for DataScience functional tests + ([#3682](https://github.com/Microsoft/vscode-python/issues/3682)) +1. Fixed language server smoke tests. + ([#3684](https://github.com/Microsoft/vscode-python/issues/3684)) +1. Add a functional test for interactive window remote connect scenario + ([#3714](https://github.com/Microsoft/vscode-python/issues/3714)) +1. Detect usage of `xonsh` shells (this does **not** add support for `xonsh` itself) + ([#3746](https://github.com/Microsoft/vscode-python/issues/3746)) +1. Remove `src/server` folder, as this is no longer required. + ([#3781](https://github.com/Microsoft/vscode-python/issues/3781)) +1. Bugfix to `pvsc-dev-ext.py` where arguments to git would not be passed on POSIX-based environments. Extended `pvsc-dev-ext.py setup` command with 2 + optional flags-- `--repo` and `--branch` to override the default git repository URL and the branch used to clone and install the extension. + (thanks [Anthony Shaw](https://github.com/tonybaloney/)) + ([#3837](https://github.com/Microsoft/vscode-python/issues/3837)) +1. Improvements to execution times of CI on Travis. + ([#3899](https://github.com/Microsoft/vscode-python/issues/3899)) +1. Add telemetry to check if global interpreter is used in workspace. + ([#3901](https://github.com/Microsoft/vscode-python/issues/3901)) +1. Make sure to search for the best Python when launching the non default interpreter. + ([#3916](https://github.com/Microsoft/vscode-python/issues/3916)) +1. Add tests for expand / collapse and hiding of cell inputs mid run + ([#3982](https://github.com/Microsoft/vscode-python/issues/3982)) +1. Move `splitParent` from `string.ts` into tests folder. + ([#3988](https://github.com/Microsoft/vscode-python/issues/3988)) +1. Ensure `debounce` decorator cannot be applied to async functions. + ([#4055](https://github.com/Microsoft/vscode-python/issues/4055)) + ## 2018.12.1 (14 Dec 2018) ### Fixes @@ -16,48 +8991,50 @@ Thanks to the following projects which we fully rely on to provide some of our features: -- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) -- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) - and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) -- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) -- [ptvsd](https://pypi.org/project/ptvsd/) -- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) -- [rope](https://pypi.org/project/rope/) (user-installed) + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) Also thanks to the various projects we provide integrations with which help make this extension useful: -- Debugging support: - [Django](https://pypi.org/project/Django/), - [Flask](https://pypi.org/project/Flask/), - [gevent](https://pypi.org/project/gevent/), - [Jinja](https://pypi.org/project/Jinja/), - [Pyramid](https://pypi.org/project/pyramid/), - [PySpark](https://pypi.org/project/pyspark/), - [Scrapy](https://pypi.org/project/Scrapy/), - [Watson](https://pypi.org/project/Watson/) -- Formatting: - [autopep8](https://pypi.org/project/autopep8/), - [black](https://pypi.org/project/black/), - [yapf](https://pypi.org/project/yapf/) -- Interpreter support: - [conda](https://conda.io/), - [direnv](https://direnv.net/), - [pipenv](https://pypi.org/project/pipenv/), - [pyenv](https://github.com/pyenv/pyenv), - [venv](https://docs.python.org/3/library/venv.html#module-venv), - [virtualenv](https://pypi.org/project/virtualenv/) -- Linting: - [bandit](https://pypi.org/project/bandit/), - [flake8](https://pypi.org/project/flake8/), - [mypy](https://pypi.org/project/mypy/), - [prospector](https://pypi.org/project/prospector/), - [pylint](https://pypi.org/project/pylint/), - [pydocstyle](https://pypi.org/project/pydocstyle/), - [pylama](https://pypi.org/project/pylama/) -- Testing: - [nose](https://pypi.org/project/nose/), - [pytest](https://pypi.org/project/pytest/), - [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +- 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 @@ -108,11 +9085,10 @@ part of! 1. Fix crash when `kernelspec` is missing path or language. ([#3561](https://github.com/Microsoft/vscode-python/issues/3561)) 1. Update the Microsoft Python Language Server to 0.1.72/[2018.12.1](https://github.com/Microsoft/python-language-server/releases/tag/2018.12.1) ([#3657](https://github.com/Microsoft/vscode-python/issues/3657)): - * Properly resolve namespace packages and relative imports. - * `Go to Definition` now supports namespace packages. - * Fixed `null` reference exceptions. - * Fixed erroneously reporting `None`, `True`, and `False` as undefined. - + - Properly resolve namespace packages and relative imports. + - `Go to Definition` now supports namespace packages. + - Fixed `null` reference exceptions. + - Fixed erroneously reporting `None`, `True`, and `False` as undefined. ### Code Health @@ -135,48 +9111,50 @@ part of! Thanks to the following projects which we fully rely on to provide some of our features: -- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) -- [jedi 0.13.1](https://pypi.org/project/jedi/0.13.1/) - and [parso 0.3.1](https://pypi.org/project/parso/0.3.1/) -- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) -- [ptvsd](https://pypi.org/project/ptvsd/) -- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) -- [rope](https://pypi.org/project/rope/) (user-installed) + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.13.1](https://pypi.org/project/jedi/0.13.1/) + and [parso 0.3.1](https://pypi.org/project/parso/0.3.1/) +- [Microsoft Python Language Server](https://github.com/microsoft/python-language-server) +- [ptvsd](https://pypi.org/project/ptvsd/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) Also thanks to the various projects we provide integrations with which help make this extension useful: -- Debugging support: - [Django](https://pypi.org/project/Django/), - [Flask](https://pypi.org/project/Flask/), - [gevent](https://pypi.org/project/gevent/), - [Jinja](https://pypi.org/project/Jinja/), - [Pyramid](https://pypi.org/project/pyramid/), - [PySpark](https://pypi.org/project/pyspark/), - [Scrapy](https://pypi.org/project/Scrapy/), - [Watson](https://pypi.org/project/Watson/) -- Formatting: - [autopep8](https://pypi.org/project/autopep8/), - [black](https://pypi.org/project/black/), - [yapf](https://pypi.org/project/yapf/) -- Interpreter support: - [conda](https://conda.io/), - [direnv](https://direnv.net/), - [pipenv](https://pypi.org/project/pipenv/), - [pyenv](https://github.com/pyenv/pyenv), - [venv](https://docs.python.org/3/library/venv.html#module-venv), - [virtualenv](https://pypi.org/project/virtualenv/) -- Linting: - [bandit](https://pypi.org/project/bandit/), - [flake8](https://pypi.org/project/flake8/), - [mypy](https://pypi.org/project/mypy/), - [prospector](https://pypi.org/project/prospector/), - [pylint](https://pypi.org/project/pylint/), - [pydocstyle](https://pypi.org/project/pydocstyle/), - [pylama](https://pypi.org/project/pylama/) -- Testing: - [nose](https://pypi.org/project/nose/), - [pytest](https://pypi.org/project/pytest/), - [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +- 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 @@ -191,8 +9169,8 @@ part of! 1. Expose an API that can be used by other extensions to interact with the Python Extension. ([#3121](https://github.com/Microsoft/vscode-python/issues/3121)) 1. Updated the language server to [0.1.65](https://github.com/Microsoft/python-language-server/releases/tag/2018.11.1): - - Improved `formatOnType` so it handles mismatched braces better - ([#3482](https://github.com/Microsoft/vscode-python/issues/3482)) + - Improved `formatOnType` so it handles mismatched braces better + ([#3482](https://github.com/Microsoft/vscode-python/issues/3482)) ### Fixes @@ -201,12 +9179,12 @@ part of! ([#793](https://github.com/Microsoft/vscode-python/issues/793)) 1. Always use bundled version of [`ptvsd`](https://github.com/microsoft/ptvsd), unless specified. To use a custom version of `ptvsd` in the debugger, add `customDebugger` into your `launch.json` configuration as follows: - ```json - "type": "python", - "request": "launch", - "customDebugger": true - ``` - ([#3283](https://github.com/Microsoft/vscode-python/issues/3283)) + ```json + "type": "python", + "request": "launch", + "customDebugger": true + ``` + ([#3283](https://github.com/Microsoft/vscode-python/issues/3283)) 1. Fix problems with virtual environments not matching the loaded python when running cells. ([#3294](https://github.com/Microsoft/vscode-python/issues/3294)) 1. Add button for interrupting the jupyter kernel @@ -220,15 +9198,15 @@ part of! 1. Re-run Jupyter notebook setup when the kernel is restarted. This correctly picks up dark color themes for matplotlib. ([#3418](https://github.com/Microsoft/vscode-python/issues/3418)) 1. Update the language server to [0.1.65](https://github.com/Microsoft/python-language-server/releases/tag/2018.11.1): - - Fixed `null` reference exception when executing "Find symbol in workspace" - - Fixed `null` argument exception that could happen when a function used tuples - - Fixed issue when variables in nested list comprehensions were marked as undefined - - Fixed exception that could be thrown with certain generic syntax - ([#3482](https://github.com/Microsoft/vscode-python/issues/3482)) + - Fixed `null` reference exception when executing "Find symbol in workspace" + - Fixed `null` argument exception that could happen when a function used tuples + - Fixed issue when variables in nested list comprehensions were marked as undefined + - Fixed exception that could be thrown with certain generic syntax + ([#3482](https://github.com/Microsoft/vscode-python/issues/3482)) ### Code Health -1. Added basic integration tests for the new Lanaguage Server. +1. Added basic integration tests for the new Language Server. ([#2041](https://github.com/Microsoft/vscode-python/issues/2041)) 1. Add smoke tests for the extension. ([#3021](https://github.com/Microsoft/vscode-python/issues/3021)) @@ -243,13 +9221,12 @@ 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 aggresive in searching for a Python environment that can run Jupyter +1. Be more aggressive in searching for a Python environment that can run Jupyter (make sure to cleanup any kernelspecs that are created during this process). ([#3433](https://github.com/Microsoft/vscode-python/issues/3433)) - ## 2018.10.1 (09 Nov 2018) ### Fixes @@ -263,48 +9240,50 @@ part of! Thanks to the following projects which we fully rely on to provide some of our features: -- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) -- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) - and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) -- Microsoft Python Language Server -- ptvsd -- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) -- [rope](https://pypi.org/project/rope/) (user-installed) + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- Microsoft Python Language Server +- ptvsd +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) Also thanks to the various projects we provide integrations with which help make this extension useful: -- Debugging support: - [Django](https://pypi.org/project/Django/), - [Flask](https://pypi.org/project/Flask/), - [gevent](https://pypi.org/project/gevent/), - [Jinja](https://pypi.org/project/Jinja/), - [Pyramid](https://pypi.org/project/pyramid/), - [PySpark](https://pypi.org/project/pyspark/), - [Scrapy](https://pypi.org/project/Scrapy/), - [Watson](https://pypi.org/project/Watson/) -- Formatting: - [autopep8](https://pypi.org/project/autopep8/), - [black](https://pypi.org/project/black/), - [yapf](https://pypi.org/project/yapf/) -- Interpreter support: - [conda](https://conda.io/), - [direnv](https://direnv.net/), - [pipenv](https://pypi.org/project/pipenv/), - [pyenv](https://github.com/pyenv/pyenv), - [venv](https://docs.python.org/3/library/venv.html#module-venv), - [virtualenv](https://pypi.org/project/virtualenv/) -- Linting: - [bandit](https://pypi.org/project/bandit/), - [flake8](https://pypi.org/project/flake8/), - [mypy](https://pypi.org/project/mypy/), - [prospector](https://pypi.org/project/prospector/), - [pylint](https://pypi.org/project/pylint/), - [pydocstyle](https://pypi.org/project/pydocstyle/), - [pylama](https://pypi.org/project/pylama/) -- Testing: - [nose](https://pypi.org/project/nose/), - [pytest](https://pypi.org/project/pytest/), - [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +- 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 @@ -331,13 +9310,13 @@ part of! 1. Updated the [language server](https://github.com/Microsoft/python-language-server) to [0.1.57/2018.11.0](https://github.com/Microsoft/python-language-server/releases/tag/2018.11.0) (from 2018.10.0) and the [debugger](https://pypi.org/project/ptvsd/) to [4.2.0](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.0) (from 4.1.3). Highlights include: - * Language server - - Completion support for [`collections.namedtuple`](https://docs.python.org/3/library/collections.html#collections.namedtuple). - - Support [`typing.NewType`](https://docs.python.org/3/library/typing.html#typing.NewType) - and [`typing.TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar). - * Debugger - - Add support for sub-process debugging (set `"subProcess": true` in your `launch.json` to use). - - Add support for [pyside2](https://pypi.org/project/PySide2/). + - Language server + - Completion support for [`collections.namedtuple`](https://docs.python.org/3/library/collections.html#collections.namedtuple). + - Support [`typing.NewType`](https://docs.python.org/3/library/typing.html#typing.NewType) + and [`typing.TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar). + - Debugger + - Add support for sub-process debugging (set `"subProcess": true` in your `launch.json` to use). + - Add support for [pyside2](https://pypi.org/project/PySide2/). 1. Add localization of strings. Localized versions are specified in the package.nls.\.json files. ([#463](https://github.com/Microsoft/vscode-python/issues/463)) 1. Clear cached list of interpreters when an interpeter is created in the workspace folder (this allows for virtual environments created in one's workspace folder to be detectable immediately). @@ -367,17 +9346,17 @@ part of! 1. Updated the [language server](https://github.com/Microsoft/python-language-server) to [0.1.57/2018.11.0](https://github.com/Microsoft/python-language-server/releases/tag/2018.11.0) (from 2018.10.0) and the [debugger](https://pypi.org/project/ptvsd/) to [4.2.0](https://github.com/Microsoft/ptvsd/releases/tag/v4.2.0) (from 4.1.3). Highlights include: - * Language server - - Completions on generic containers work (e.g. `x: List[T]` now have completions for `x`, not just `x[]`). - - Fixed issues relating to `Go to Definition` for `from ... import` statements. - - `None` is no longer flagged as undefined. - - `BadSourceException` should no longer be raised. - - Fixed a null reference exception when handling certain function overloads. - * Debugger - - Properly deal with handled or unhandled exception in top level frames. - - Any folder ending with `site-packages` is considered a library. - - Treat any code not in `site-packages` as user code. - - Handle case where no completions are provided by the debugger. + - Language server + - Completions on generic containers work (e.g. `x: List[T]` now have completions for `x`, not just `x[]`). + - Fixed issues relating to `Go to Definition` for `from ... import` statements. + - `None` is no longer flagged as undefined. + - `BadSourceException` should no longer be raised. + - Fixed a null reference exception when handling certain function overloads. + - Debugger + - Properly deal with handled or unhandled exception in top level frames. + - Any folder ending with `site-packages` is considered a library. + - Treat any code not in `site-packages` as user code. + - Handle case where no completions are provided by the debugger. ### Code Health @@ -409,10 +9388,6 @@ part of! 1. Pin extension to a minimum version of the language server. ([#3125](https://github.com/Microsoft/vscode-python/issues/3125)) - - - - ## 2018.9.2 (29 Oct 2018) ### Fixes @@ -425,7 +9400,6 @@ part of! 1. Forward telemetry from the language server. ([#2940](https://github.com/Microsoft/vscode-python/issues/2940)) - ## 2018.9.1 (18 Oct 2018) ### Fixes @@ -442,55 +9416,56 @@ part of! 1. Add ability to publish extension builds from `release` branches into the blob store. ([#2874](https://github.com/Microsoft/vscode-python/issues/2874)) - ## 2018.9.0 (9 Oct 2018) ### Thanks Thanks to the following projects which we fully rely on to provide some of our features: -- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) -- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) - and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) -- [Microsoft Python Language Server 2018.9.0](https://github.com/Microsoft/python-language-server/releases/tag/2018.9.0) -- [ptvsd 4.1.3](https://github.com/Microsoft/ptvsd/releases/tag/v4.1.3) -- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) -- [rope](https://pypi.org/project/rope/) (user-installed) + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [Microsoft Python Language Server 2018.9.0](https://github.com/Microsoft/python-language-server/releases/tag/2018.9.0) +- [ptvsd 4.1.3](https://github.com/Microsoft/ptvsd/releases/tag/v4.1.3) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) Also thanks to the various projects we provide integrations with which help make this extension useful: -- Debugging support: - [Django](https://pypi.org/project/Django/), - [Flask](https://pypi.org/project/Flask/), - [gevent](https://pypi.org/project/gevent/), - [Jinja](https://pypi.org/project/Jinja/), - [Pyramid](https://pypi.org/project/pyramid/), - [PySpark](https://pypi.org/project/pyspark/), - [Scrapy](https://pypi.org/project/Scrapy/), - [Watson](https://pypi.org/project/Watson/) -- Formatting: - [autopep8](https://pypi.org/project/autopep8/), - [black](https://pypi.org/project/black/), - [yapf](https://pypi.org/project/yapf/) -- Interpreter support: - [conda](https://conda.io/), - [direnv](https://direnv.net/), - [pipenv](https://pypi.org/project/pipenv/), - [pyenv](https://github.com/pyenv/pyenv), - [venv](https://docs.python.org/3/library/venv.html#module-venv), - [virtualenv](https://pypi.org/project/virtualenv/) -- Linting: - [bandit](https://pypi.org/project/bandit/), - [flake8](https://pypi.org/project/flake8/), - [mypy](https://pypi.org/project/mypy/), - [prospector](https://pypi.org/project/prospector/), - [pylint](https://pypi.org/project/pylint/), - [pydocstyle](https://pypi.org/project/pydocstyle/), - [pylama](https://pypi.org/project/pylama/) -- Testing: - [nose](https://pypi.org/project/nose/), - [pytest](https://pypi.org/project/pytest/), - [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +- 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 @@ -546,7 +9521,7 @@ part of! 1. Fix the regex expression to match MyPy linter messages that expects the file name to have a `.py` extension, that isn't always the case, to catch any filename. E.g., .pyi files that describes interfaces wouldn't get the linter messages to Problems tab. ([#2380](https://github.com/Microsoft/vscode-python/issues/2380)) -1. Do not use variable substitution when updating `python.pythonPath`. This matters +1. Do not use variable substitution when updating `python.pythonPath`. This matters because VS Code does not do variable substitution in settings values. ([#2459](https://github.com/Microsoft/vscode-python/issues/2459)) 1. Use a python script to launch the debugger, instead of using `-m` which requires changes to the `PYTHONPATH` variable. @@ -581,53 +9556,54 @@ part of! 1. Update `vscode-extension-telemetry` to `0.0.22`. ([#2745](https://github.com/Microsoft/vscode-python/issues/2745)) - ## 2018.8.0 (04 September 2018) ### Thanks Thanks to the following projects which we fully rely on to provide some of our features: -- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) -- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) - and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) -- [4.1.1](https://pypi.org/project/ptvsd/4.1.1/) -- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) -- [rope](https://pypi.org/project/rope/) (user-installed) + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [4.1.1](https://pypi.org/project/ptvsd/4.1.1/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) Also thanks to the various projects we provide integrations with which help make this extension useful: -- Debugging support: - [Django](https://pypi.org/project/Django/), - [Flask](https://pypi.org/project/Flask/), - [gevent](https://pypi.org/project/gevent/), - [Jinja](https://pypi.org/project/Jinja/), - [Pyramid](https://pypi.org/project/pyramid/), - [PySpark](https://pypi.org/project/pyspark/), - [Scrapy](https://pypi.org/project/Scrapy/), - [Watson](https://pypi.org/project/Watson/) -- Formatting: - [autopep8](https://pypi.org/project/autopep8/), - [black](https://pypi.org/project/black/), - [yapf](https://pypi.org/project/yapf/) -- Interpreter support: - [conda](https://conda.io/), - [direnv](https://direnv.net/), - [pipenv](https://pypi.org/project/pipenv/), - [pyenv](https://github.com/pyenv/pyenv), - [venv](https://docs.python.org/3/library/venv.html#module-venv), - [virtualenv](https://pypi.org/project/virtualenv/) -- Linting: - [flake8](https://pypi.org/project/flake8/), - [mypy](https://pypi.org/project/mypy/), - [prospector](https://pypi.org/project/prospector/), - [pylint](https://pypi.org/project/pylint/), - [pydocstyle](https://pypi.org/project/pydocstyle/), - [pylama](https://pypi.org/project/pylama/) -- Testing: - [nose](https://pypi.org/project/nose/), - [pytest](https://pypi.org/project/pytest/), - [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) And finally thanks to the [Python](https://www.python.org/) development team and community for creating a fantastic programming language and community to be a @@ -726,14 +9702,13 @@ part of! ([#2266](https://github.com/Microsoft/vscode-python/issues/2266)) 1. Deprecate command `Python: Build Workspace Symbols` when using the language server. ([#2267](https://github.com/Microsoft/vscode-python/issues/2267)) -1. Pin version of `pylint` to `3.6.3` to allow ensure `pylint` gets installed on Travis with Pytnon2.7. +1. Pin version of `pylint` to `3.6.3` to allow ensure `pylint` gets installed on Travis with Python2.7. ([#2305](https://github.com/Microsoft/vscode-python/issues/2305)) 1. Remove some of the debugger tests and fix some minor debugger issues. ([#2307](https://github.com/Microsoft/vscode-python/issues/2307)) 1. Only use the current stable version of PTVSD in CI builds/releases. ([#2432](https://github.com/Microsoft/vscode-python/issues/2432)) - ## 2018.7.1 (23 July 2018) ### Fixes @@ -742,53 +9717,54 @@ part of! [651468731500ec1cc644029c3666c57b82f77d76](https://github.com/Microsoft/PTVS/commit/651468731500ec1cc644029c3666c57b82f77d76). ([#2233](https://github.com/Microsoft/vscode-python/issues/2233)) - ## 2018.7.0 (18 July 2018) ### Thanks Thanks to the following projects which we fully rely on to provide some of our features: -- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) -- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) - and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) -- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.11a5](https://pypi.org/project/ptvsd/4.1.11a5/) -- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) -- [rope](https://pypi.org/project/rope/) (user-installed) + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.11a5](https://pypi.org/project/ptvsd/4.1.11a5/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) Also thanks to the various projects we provide integrations with which help make this extension useful: -- Debugging support: - [Django](https://pypi.org/project/Django/), - [Flask](https://pypi.org/project/Flask/), - [gevent](https://pypi.org/project/gevent/), - [Jinja](https://pypi.org/project/Jinja/), - [Pyramid](https://pypi.org/project/pyramid/), - [PySpark](https://pypi.org/project/pyspark/), - [Scrapy](https://pypi.org/project/Scrapy/), - [Watson](https://pypi.org/project/Watson/) -- Formatting: - [autopep8](https://pypi.org/project/autopep8/), - [black](https://pypi.org/project/black/), - [yapf](https://pypi.org/project/yapf/) -- Interpreter support: - [conda](https://conda.io/), - [direnv](https://direnv.net/), - [pipenv](https://pypi.org/project/pipenv/), - [pyenv](https://github.com/pyenv/pyenv), - [venv](https://docs.python.org/3/library/venv.html#module-venv), - [virtualenv](https://pypi.org/project/virtualenv/) -- Linting: - [flake8](https://pypi.org/project/flake8/), - [mypy](https://pypi.org/project/mypy/), - [prospector](https://pypi.org/project/prospector/), - [pylint](https://pypi.org/project/pylint/), - [pydocstyle](https://pypi.org/project/pydocstyle/), - [pylama](https://pypi.org/project/pylama/) -- Testing: - [nose](https://pypi.org/project/nose/), - [pytest](https://pypi.org/project/pytest/), - [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) And finally thanks to the [Python](https://www.python.org/) development team and community for creating a fantastic programming language and community to be a @@ -841,54 +9817,54 @@ part of! 1. Change the download links of the language server files. ([#2180](https://github.com/Microsoft/vscode-python/issues/2180)) - - ## 2018.6.0 (20 June 2018) ### Thanks Thanks to the following projects which we fully rely on to provide some of our features: -- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) -- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) - and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) -- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.11a5](https://pypi.org/project/ptvsd/4.1.11a5/) -- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) -- [rope](https://pypi.org/project/rope/) (user-installed) + +- [isort 4.3.4](https://pypi.org/project/isort/4.3.4/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.1](https://pypi.org/project/parso/0.2.1/) +- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.11a5](https://pypi.org/project/ptvsd/4.1.11a5/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) Also thanks to the various projects we provide integrations with which help make this extension useful: -- Debugging support: - [Django](https://pypi.org/project/Django/), - [Flask](https://pypi.org/project/Flask/), - [gevent](https://pypi.org/project/gevent/), - [Jinja](https://pypi.org/project/Jinja/), - [Pyramid](https://pypi.org/project/pyramid/), - [PySpark](https://pypi.org/project/pyspark/), - [Scrapy](https://pypi.org/project/Scrapy/), - [Watson](https://pypi.org/project/Watson/) -- Formatting: - [autopep8](https://pypi.org/project/autopep8/), - [black](https://pypi.org/project/black/), - [yapf](https://pypi.org/project/yapf/) -- Interpreter support: - [conda](https://conda.io/), - [direnv](https://direnv.net/), - [pipenv](https://pypi.org/project/pipenv/), - [pyenv](https://github.com/pyenv/pyenv), - [venv](https://docs.python.org/3/library/venv.html#module-venv), - [virtualenv](https://pypi.org/project/virtualenv/) -- Linting: - [flake8](https://pypi.org/project/flake8/), - [mypy](https://pypi.org/project/mypy/), - [prospector](https://pypi.org/project/prospector/), - [pylint](https://pypi.org/project/pylint/), - [pydocstyle](https://pypi.org/project/pydocstyle/), - [pylama](https://pypi.org/project/pylama/) -- Testing: - [nose](https://pypi.org/project/nose/), - [pytest](https://pypi.org/project/pytest/), - [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) + +- Debugging support: + [Django](https://pypi.org/project/Django/), + [Flask](https://pypi.org/project/Flask/), + [gevent](https://pypi.org/project/gevent/), + [Jinja](https://pypi.org/project/Jinja/), + [Pyramid](https://pypi.org/project/pyramid/), + [PySpark](https://pypi.org/project/pyspark/), + [Scrapy](https://pypi.org/project/Scrapy/), + [Watson](https://pypi.org/project/Watson/) +- Formatting: + [autopep8](https://pypi.org/project/autopep8/), + [black](https://pypi.org/project/black/), + [yapf](https://pypi.org/project/yapf/) +- Interpreter support: + [conda](https://conda.io/), + [direnv](https://direnv.net/), + [pipenv](https://pypi.org/project/pipenv/), + [pyenv](https://github.com/pyenv/pyenv), + [venv](https://docs.python.org/3/library/venv.html#module-venv), + [virtualenv](https://pypi.org/project/virtualenv/) +- Linting: + [flake8](https://pypi.org/project/flake8/), + [mypy](https://pypi.org/project/mypy/), + [prospector](https://pypi.org/project/prospector/), + [pylint](https://pypi.org/project/pylint/), + [pydocstyle](https://pypi.org/project/pydocstyle/), + [pylama](https://pypi.org/project/pylama/) +- Testing: + [nose](https://pypi.org/project/nose/), + [pytest](https://pypi.org/project/pytest/), + [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) And finally thanks to the [Python](https://www.python.org/) development team and community for creating a fantastic programming language and community to be a @@ -903,14 +9879,14 @@ part of! (thanks [Bence Nagy](https://github.com/underyx)) ([#127](https://github.com/Microsoft/vscode-python/issues/127)) 1. Add support for the `"source.organizeImports"` setting for `"editor.codeActionsOnSave"` (thanks [Nathan Gaberel](https://github.com/n6g7)); you can turn this on just for Python using: - ```json - "[python]": { - "editor.codeActionsOnSave": { - "source.organizeImports": true - } - } - ``` - ([#156](https://github.com/Microsoft/vscode-python/issues/156)) + ```json + "[python]": { + "editor.codeActionsOnSave": { + "source.organizeImports": true + } + } + ``` + ([#156](https://github.com/Microsoft/vscode-python/issues/156)) 1. Added Spanish translation. (thanks [Mario Rubio](https://github.com/mario-mra/)) ([#1902](https://github.com/Microsoft/vscode-python/issues/1902)) @@ -929,7 +9905,7 @@ part of! ([#1064](https://github.com/Microsoft/vscode-python/issues/1064)) 1. Improvements to the logic used to parse the arguments passed into the test frameworks. ([#1070](https://github.com/Microsoft/vscode-python/issues/1070)) -1. Ensure navigation to definitons follows imports and is transparent to decoration. +1. Ensure navigation to definitions follows imports and is transparent to decoration. (thanks [Peter Law](https://github.com/PeterJCLaw)) ([#1638](https://github.com/Microsoft/vscode-python/issues/1638)) 1. Fix for intellisense failing when using the new `Outline` feature. @@ -1013,20 +9989,17 @@ part of! 1. Create tests to measure activation times for the extension. ([#932](https://github.com/Microsoft/vscode-python/issues/932)) - - - - ## 2018.5.0 (05 Jun 2018) Thanks to the following projects which we fully rely on to provide some of our features: -- [isort 4.2.15](https://pypi.org/project/isort/4.2.15/) -- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) - and [parso 0.2.0](https://pypi.org/project/parso/0.2.0/) -- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.1a5](https://pypi.org/project/ptvsd/4.1.1a5/) -- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) -- [rope](https://pypi.org/project/rope/) (user-installed) + +- [isort 4.2.15](https://pypi.org/project/isort/4.2.15/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.0](https://pypi.org/project/parso/0.2.0/) +- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.1a5](https://pypi.org/project/ptvsd/4.1.1a5/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) And thanks to the many other projects which users can optionally choose from and install to work with the extension. Without them the extension would not be @@ -1072,7 +10045,7 @@ nearly as feature-rich and useful as it is. ([#452](https://github.com/Microsoft/vscode-python/issues/452)) 1. Ensure empty paths do not get added into `sys.path` by the Jedi language server. (this was fixed in the previous release in [#1471](https://github.com/Microsoft/vscode-python/pull/1471)) ([#677](https://github.com/Microsoft/vscode-python/issues/677)) -1. Resolves rename refactor issue that remvoes the last line of the source file when the line is being refactored and source does not end with an EOL. +1. Resolves rename refactor issue that removes the last line of the source file when the line is being refactored and source does not end with an EOL. ([#695](https://github.com/Microsoft/vscode-python/issues/695)) 1. Ensure the prompt to install missing packages is not displayed more than once. ([#980](https://github.com/Microsoft/vscode-python/issues/980)) @@ -1119,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)) @@ -1129,23 +10102,20 @@ nearly as feature-rich and useful as it is. ([#1794](https://github.com/Microsoft/vscode-python/issues/1794)) 1. Fix failing Prospector unit tests and add more tests for linters (with and without workspaces). ([#1836](https://github.com/Microsoft/vscode-python/issues/1836)) -1. Ensure `Outline` view doesn't overload the language server with too many requets, while user is editing text in the editor. +1. Ensure `Outline` view doesn't overload the language server with too many requests, while user is editing text in the editor. ([#1856](https://github.com/Microsoft/vscode-python/issues/1856)) - - - - ## 2018.4.0 (2 May 2018) Thanks to the following projects which we fully rely on to provide some of our features: -- [isort 4.2.15](https://pypi.org/project/isort/4.2.15/) -- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) - and [parso 0.2.0](https://pypi.org/project/parso/0.2.0/) -- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.1a1](https://pypi.org/project/ptvsd/4.1.1a1/) -- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) -- [rope](https://pypi.org/project/rope/) (user-installed) + +- [isort 4.2.15](https://pypi.org/project/isort/4.2.15/) +- [jedi 0.12.0](https://pypi.org/project/jedi/0.12.0/) + and [parso 0.2.0](https://pypi.org/project/parso/0.2.0/) +- [ptvsd 3.0.0](https://pypi.org/project/ptvsd/3.0.0/) and [4.1.1a1](https://pypi.org/project/ptvsd/4.1.1a1/) +- [exuberant ctags](http://ctags.sourceforge.net/) (user-installed) +- [rope](https://pypi.org/project/rope/) (user-installed) And a special thanks to [Patryk Zawadzki](https://github.com/patrys) for all of his help on [our issue tracker](https://github.com/Microsoft/vscode-python)! @@ -1154,12 +10124,12 @@ his help on [our issue tracker](https://github.com/Microsoft/vscode-python)! 1. Enable debugging of Jinja templates in the experimental debugger. This is made possible with the addition of the `jinja` setting in the `launch.json` file as follows: - ```json - "request": "launch or attach", - ... - "jinja": true - ``` - ([#1206](https://github.com/Microsoft/vscode-python/issues/1206)) + ```json + "request": "launch or attach", + ... + "jinja": true + ``` + ([#1206](https://github.com/Microsoft/vscode-python/issues/1206)) 1. Remove empty spaces from the selected text of the active editor when executing in a terminal. ([#1207](https://github.com/Microsoft/vscode-python/issues/1207)) 1. Add prelimnary support for remote debugging using the experimental debugger. @@ -1179,21 +10149,21 @@ his help on [our issue tracker](https://github.com/Microsoft/vscode-python)! ([#1395](https://github.com/Microsoft/vscode-python/issues/1395)) 1. Intergrate Jedi 0.12. See https://github.com/davidhalter/jedi/issues/1063#issuecomment-381417297 for details. ([#1400](https://github.com/Microsoft/vscode-python/issues/1400)) -1. Enable Jinja template debugging as a default behaivour when using the Watson debug configuration for debugging of Watson applications. +1. Enable Jinja template debugging as a default behaviour when using the Watson debug configuration for debugging of Watson applications. ([#1480](https://github.com/Microsoft/vscode-python/issues/1480)) 1. Enable Jinja template debugging as a default behavior when debugging Pyramid applications. ([#1492](https://github.com/Microsoft/vscode-python/issues/1492)) 1. Add prelimnary support for remote debugging using the experimental debugger. Attach to a Python program after having imported `ptvsd` and enabling the debugger to attach as follows: - ```python - import ptvsd - ptvsd.enable_attach(('0.0.0.0', 5678)) - ``` - Additional capabilities: - * `ptvsd.break_into_debugger()` to break into the attached debugger. - * `ptvsd.wait_for_attach(timeout)` to cause the program to wait untill a debugger attaches. - * `ptvsd.is_attached()` to determine whether a debugger is attached to the program. - ([#907](https://github.com/Microsoft/vscode-python/issues/907)) + ```python + import ptvsd + ptvsd.enable_attach(('0.0.0.0', 5678)) + ``` + Additional capabilities: + - `ptvsd.break_into_debugger()` to break into the attached debugger. + - `ptvsd.wait_for_attach(timeout)` to cause the program to wait until a debugger attaches. + - `ptvsd.is_attached()` to determine whether a debugger is attached to the program. + ([#907](https://github.com/Microsoft/vscode-python/issues/907)) ### Fixes @@ -1205,7 +10175,7 @@ his help on [our issue tracker](https://github.com/Microsoft/vscode-python)! ([#1072](https://github.com/Microsoft/vscode-python/issues/1072)) 1. Reverted change that ended up considering symlinked interpreters as duplicate interpreter. ([#1192](https://github.com/Microsoft/vscode-python/issues/1192)) -1. Display errors returned by the PipEnv command when identifying the corresonding environment. +1. Display errors returned by the PipEnv command when identifying the corresponding environment. ([#1254](https://github.com/Microsoft/vscode-python/issues/1254)) 1. When `editor.formatOnType` is on, don't add a space for `*args` or `**kwargs` ([#1257](https://github.com/Microsoft/vscode-python/issues/1257)) @@ -1252,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) @@ -1271,118 +10241,117 @@ his help on [our issue tracker](https://github.com/Microsoft/vscode-python)! 1. Register language server functionality in the extension against specific resource types supporting the python language. ([#1530](https://github.com/Microsoft/vscode-python/issues/1530)) - ## 2018.3.1 (29 Mar 2018) ### Fixes 1. Fixes issue that causes linter to fail when file path contains spaces. -([#1239](https://github.com/Microsoft/vscode-python/issues/1239)) + ([#1239](https://github.com/Microsoft/vscode-python/issues/1239)) ## 2018.3.0 (28 Mar 2018) ### Enhancements 1. Add a PySpark debug configuration for the experimental debugger. - ([#1029](https://github.com/Microsoft/vscode-python/issues/1029)) + ([#1029](https://github.com/Microsoft/vscode-python/issues/1029)) 1. Add a Pyramid debug configuration for the experimental debugger. - ([#1030](https://github.com/Microsoft/vscode-python/issues/1030)) + ([#1030](https://github.com/Microsoft/vscode-python/issues/1030)) 1. Add a Watson debug configuration for the experimental debugger. - ([#1031](https://github.com/Microsoft/vscode-python/issues/1031)) + ([#1031](https://github.com/Microsoft/vscode-python/issues/1031)) 1. Add a Scrapy debug configuration for the experimental debugger. - ([#1032](https://github.com/Microsoft/vscode-python/issues/1032)) + ([#1032](https://github.com/Microsoft/vscode-python/issues/1032)) 1. When using pipenv, install packages (such as linters, test frameworks) in dev-packages. - ([#1110](https://github.com/Microsoft/vscode-python/issues/1110)) + ([#1110](https://github.com/Microsoft/vscode-python/issues/1110)) 1. Added commands translation for italian locale. -(thanks [Dotpys](https://github.com/Dotpys/)) ([#1152](https://github.com/Microsoft/vscode-python/issues/1152)) + (thanks [Dotpys](https://github.com/Dotpys/)) ([#1152](https://github.com/Microsoft/vscode-python/issues/1152)) 1. Add support for Django Template debugging in experimental debugger. - ([#1189](https://github.com/Microsoft/vscode-python/issues/1189)) + ([#1189](https://github.com/Microsoft/vscode-python/issues/1189)) 1. Add support for Flask Template debugging in experimental debugger. - ([#1190](https://github.com/Microsoft/vscode-python/issues/1190)) + ([#1190](https://github.com/Microsoft/vscode-python/issues/1190)) 1. Add support for Jinja template debugging. ([#1210](https://github.com/Microsoft/vscode-python/issues/1210)) 1. When debugging, use `Integrated Terminal` as the default console. - ([#526](https://github.com/Microsoft/vscode-python/issues/526)) + ([#526](https://github.com/Microsoft/vscode-python/issues/526)) 1. Disable the display of errors messages when rediscovering of tests fail in response to changes to files, e.g. don't show a message if there's a syntax error in the test code. - ([#704](https://github.com/Microsoft/vscode-python/issues/704)) -1. Bundle python depedencies (PTVSD package) in the extension for the experimental debugger. - ([#741](https://github.com/Microsoft/vscode-python/issues/741)) -1. Add support for expermental debugger when debugging Python Unit Tests. - ([#906](https://github.com/Microsoft/vscode-python/issues/906)) + ([#704](https://github.com/Microsoft/vscode-python/issues/704)) +1. Bundle python dependencies (PTVSD package) in the extension for the experimental debugger. + ([#741](https://github.com/Microsoft/vscode-python/issues/741)) +1. Add support for experimental debugger when debugging Python Unit Tests. + ([#906](https://github.com/Microsoft/vscode-python/issues/906)) 1. Support `Debug Console` as a `console` option for the Experimental Debugger. - ([#950](https://github.com/Microsoft/vscode-python/issues/950)) + ([#950](https://github.com/Microsoft/vscode-python/issues/950)) 1. Enable syntax highlighting for `requirements.in` files as used by -e.g. [pip-tools](https://github.com/jazzband/pip-tools) -(thanks [Lorenzo Villani](https://github.com/lvillani)) - ([#961](https://github.com/Microsoft/vscode-python/issues/961)) + e.g. [pip-tools](https://github.com/jazzband/pip-tools) + (thanks [Lorenzo Villani](https://github.com/lvillani)) + ([#961](https://github.com/Microsoft/vscode-python/issues/961)) 1. Add support to read name of Pipfile from environment variable. - ([#999](https://github.com/Microsoft/vscode-python/issues/999)) + ([#999](https://github.com/Microsoft/vscode-python/issues/999)) ### Fixes 1. Fixes issue that causes debugging of unit tests to hang indefinitely. ([#1009](https://github.com/Microsoft/vscode-python/issues/1009)) 1. Add ability to disable the check on memory usage of language server (Jedi) process. -To turn off this check, add `"python.jediMemoryLimit": -1` to your user or workspace settings (`settings.json`) file. - ([#1036](https://github.com/Microsoft/vscode-python/issues/1036)) + To turn off this check, add `"python.jediMemoryLimit": -1` to your user or workspace settings (`settings.json`) file. + ([#1036](https://github.com/Microsoft/vscode-python/issues/1036)) 1. Ignore test results when debugging unit tests. - ([#1043](https://github.com/Microsoft/vscode-python/issues/1043)) + ([#1043](https://github.com/Microsoft/vscode-python/issues/1043)) 1. Fixes auto formatting of conditional statements containing expressions with `<=` symbols. - ([#1096](https://github.com/Microsoft/vscode-python/issues/1096)) + ([#1096](https://github.com/Microsoft/vscode-python/issues/1096)) 1. Resolve debug configuration information in `launch.json` when debugging without opening a python file. - ([#1098](https://github.com/Microsoft/vscode-python/issues/1098)) + ([#1098](https://github.com/Microsoft/vscode-python/issues/1098)) 1. Disables auto completion when editing text at the end of a comment string. - ([#1123](https://github.com/Microsoft/vscode-python/issues/1123)) + ([#1123](https://github.com/Microsoft/vscode-python/issues/1123)) 1. Ensures file paths are properly encoded when passing them as arguments to linters. - ([#199](https://github.com/Microsoft/vscode-python/issues/199)) + ([#199](https://github.com/Microsoft/vscode-python/issues/199)) 1. Fix occasionally having unverified breakpoints - ([#87](https://github.com/Microsoft/vscode-python/issues/87)) + ([#87](https://github.com/Microsoft/vscode-python/issues/87)) 1. Ensure conda installer is not used for non-conda environments. - ([#969](https://github.com/Microsoft/vscode-python/issues/969)) + ([#969](https://github.com/Microsoft/vscode-python/issues/969)) 1. Fixes issue that display incorrect interpreter briefly before updating it to the right value. - ([#981](https://github.com/Microsoft/vscode-python/issues/981)) + ([#981](https://github.com/Microsoft/vscode-python/issues/981)) ### Code Health 1. Exclude 'news' folder from getting packaged into the extension. - ([#1020](https://github.com/Microsoft/vscode-python/issues/1020)) + ([#1020](https://github.com/Microsoft/vscode-python/issues/1020)) 1. Remove Jupyter commands. -(thanks [Yu Zhang](https://github.com/neilsustc)) - ([#1034](https://github.com/Microsoft/vscode-python/issues/1034)) + (thanks [Yu Zhang](https://github.com/neilsustc)) + ([#1034](https://github.com/Microsoft/vscode-python/issues/1034)) 1. Trigger incremental build compilation only when typescript files are modified. - ([#1040](https://github.com/Microsoft/vscode-python/issues/1040)) + ([#1040](https://github.com/Microsoft/vscode-python/issues/1040)) 1. Updated npm dependencies in devDependencies and fix TypeScript compilation issues. - ([#1042](https://github.com/Microsoft/vscode-python/issues/1042)) + ([#1042](https://github.com/Microsoft/vscode-python/issues/1042)) 1. Enable unit testing of stdout and stderr redirection for the experimental debugger. - ([#1048](https://github.com/Microsoft/vscode-python/issues/1048)) + ([#1048](https://github.com/Microsoft/vscode-python/issues/1048)) 1. Update npm package `vscode-extension-telemetry` to fix the warning 'os.tmpDir() deprecation'. -(thanks [osya](https://github.com/osya)) - ([#1066](https://github.com/Microsoft/vscode-python/issues/1066)) + (thanks [osya](https://github.com/osya)) + ([#1066](https://github.com/Microsoft/vscode-python/issues/1066)) 1. Prevent the debugger stepping into JS code while developing the extension when debugging async TypeScript code. - ([#1090](https://github.com/Microsoft/vscode-python/issues/1090)) + ([#1090](https://github.com/Microsoft/vscode-python/issues/1090)) 1. Increase timeouts for the debugger unit tests. - ([#1094](https://github.com/Microsoft/vscode-python/issues/1094)) + ([#1094](https://github.com/Microsoft/vscode-python/issues/1094)) 1. Change the command used to install pip on AppVeyor to avoid installation errors. - ([#1107](https://github.com/Microsoft/vscode-python/issues/1107)) + ([#1107](https://github.com/Microsoft/vscode-python/issues/1107)) 1. Check whether a document is active when detecthing changes in the active document. - ([#1114](https://github.com/Microsoft/vscode-python/issues/1114)) + ([#1114](https://github.com/Microsoft/vscode-python/issues/1114)) 1. Remove SIGINT handler in debugger adapter, thereby preventing it from shutting down the debugger. - ([#1122](https://github.com/Microsoft/vscode-python/issues/1122)) + ([#1122](https://github.com/Microsoft/vscode-python/issues/1122)) 1. Improve compilation speed of the extension's TypeScript code. - ([#1146](https://github.com/Microsoft/vscode-python/issues/1146)) + ([#1146](https://github.com/Microsoft/vscode-python/issues/1146)) 1. Changes to how debug options are passed into the experimental version of PTVSD (debugger). - ([#1168](https://github.com/Microsoft/vscode-python/issues/1168)) + ([#1168](https://github.com/Microsoft/vscode-python/issues/1168)) 1. Ensure file paths are not sent in telemetry when running unit tests. - ([#1180](https://github.com/Microsoft/vscode-python/issues/1180)) + ([#1180](https://github.com/Microsoft/vscode-python/issues/1180)) 1. Change `DjangoDebugging` to `Django` in `debugOptions` of launch.json. - ([#1198](https://github.com/Microsoft/vscode-python/issues/1198)) + ([#1198](https://github.com/Microsoft/vscode-python/issues/1198)) 1. Changed property name used to capture the trigger source of Unit Tests. ([#1213](https://github.com/Microsoft/vscode-python/issues/1213)) 1. Enable unit testing of the experimental debugger on CI servers - ([#742](https://github.com/Microsoft/vscode-python/issues/742)) + ([#742](https://github.com/Microsoft/vscode-python/issues/742)) 1. Generate code coverage for debug adapter unit tests. - ([#778](https://github.com/Microsoft/vscode-python/issues/778)) + ([#778](https://github.com/Microsoft/vscode-python/issues/778)) 1. Execute prospector as a module (using -m). - ([#982](https://github.com/Microsoft/vscode-python/issues/982)) + ([#982](https://github.com/Microsoft/vscode-python/issues/982)) 1. Launch unit tests in debug mode as opposed to running and attaching the debugger to the already-running interpreter. - ([#983](https://github.com/Microsoft/vscode-python/issues/983)) + ([#983](https://github.com/Microsoft/vscode-python/issues/983)) ## 2018.2.1 (09 Mar 2018) @@ -1403,11 +10372,11 @@ those who reported bugs or provided feedback)! A special thanks goes out to the following external contributors who contributed code in this release: -* [Andrea D'Amore](https://github.com/Microsoft/vscode-python/commits?author=anddam) -* [Tzu-ping Chung](https://github.com/Microsoft/vscode-python/commits?author=uranusjr) -* [Elliott Beach](https://github.com/Microsoft/vscode-python/commits?author=elliott-beach) -* [Manuja Jay](https://github.com/Microsoft/vscode-python/commits?author=manujadev) -* [philipwasserman](https://github.com/Microsoft/vscode-python/commits?author=philipwasserman) +- [Andrea D'Amore](https://github.com/Microsoft/vscode-python/commits?author=anddam) +- [Tzu-ping Chung](https://github.com/Microsoft/vscode-python/commits?author=uranusjr) +- [Elliott Beach](https://github.com/Microsoft/vscode-python/commits?author=elliott-beach) +- [Manuja Jay](https://github.com/Microsoft/vscode-python/commits?author=manujadev) +- [philipwasserman](https://github.com/Microsoft/vscode-python/commits?author=philipwasserman) ### Enhancements @@ -1441,9 +10410,9 @@ contributed code in this release: 1. Better detection of a `pylintrc` is available to automatically disable our default Pylint checks ([#728](https://github.com/Microsoft/vscode-python/issues/728), - [#788](https://github.com/Microsoft/vscode-python/issues/788), - [#838](https://github.com/Microsoft/vscode-python/issues/838), - [#442](https://github.com/Microsoft/vscode-python/issues/442)) + [#788](https://github.com/Microsoft/vscode-python/issues/788), + [#838](https://github.com/Microsoft/vscode-python/issues/838), + [#442](https://github.com/Microsoft/vscode-python/issues/442)) 1. Fix `Got to Python object` ([#403](https://github.com/Microsoft/vscode-python/issues/403)) 1. When reformatting a file, put the temporary file in the workspace folder so e.g. yapf detect their configuration files appropriately @@ -1466,14 +10435,14 @@ contributed code in this release: automatically killing the process; reload VS Code to start the process again if desired ([#926](https://github.com/Microsoft/vscode-python/issues/926), - [#263](https://github.com/Microsoft/vscode-python/issues/263)) + [#263](https://github.com/Microsoft/vscode-python/issues/263)) 1. Support multiple linters again ([#913](https://github.com/Microsoft/vscode-python/issues/913)) 1. Don't over-escape markup found in docstrings ([#911](https://github.com/Microsoft/vscode-python/issues/911), - [#716](https://github.com/Microsoft/vscode-python/issues/716), - [#627](https://github.com/Microsoft/vscode-python/issues/627), - [#692](https://github.com/Microsoft/vscode-python/issues/692)) + [#716](https://github.com/Microsoft/vscode-python/issues/716), + [#627](https://github.com/Microsoft/vscode-python/issues/627), + [#692](https://github.com/Microsoft/vscode-python/issues/692)) 1. Fix when the `Problems` pane lists file paths prefixed with `git:` ([#916](https://github.com/Microsoft/vscode-python/issues/916)) 1. Fix inline documentation when an odd number of quotes exists @@ -1489,8 +10458,8 @@ contributed code in this release: 1. Upgrade to Jedi 0.11.1 ([#674](https://github.com/Microsoft/vscode-python/issues/674), - [#607](https://github.com/Microsoft/vscode-python/issues/607), - [#99](https://github.com/Microsoft/vscode-python/issues/99)) + [#607](https://github.com/Microsoft/vscode-python/issues/607), + [#99](https://github.com/Microsoft/vscode-python/issues/99)) 1. Removed the banner announcing the extension moving over to Microsoft ([#830](https://github.com/Microsoft/vscode-python/issues/830)) 1. Renamed the default debugger configurations ([#412](https://github.com/Microsoft/vscode-python/issues/412)) @@ -1504,607 +10473,667 @@ contributed code in this release: Thanks to everyone who contributed to this release, including the following people who contributed code: -* [jpfarias](https://github.com/jpfarias) -* [Hongbo He](https://github.com/graycarl) -* [JohnstonCode](https://github.com/JohnstonCode) -* [Yuichi Nukiyama](https://github.com/YuichiNukiyama) -* [MichaelSuen](https://github.com/MichaelSuen-thePointer) +- [jpfarias](https://github.com/jpfarias) +- [Hongbo He](https://github.com/graycarl) +- [JohnstonCode](https://github.com/JohnstonCode) +- [Yuichi Nukiyama](https://github.com/YuichiNukiyama) +- [MichaelSuen](https://github.com/MichaelSuen-thePointer) ### Fixed issues -* Support cached interpreter locations for faster interpreter selection ([#666](https://github.com/Microsoft/vscode-python/issues/259)) -* Sending a block of code with multiple global-level scopes now works ([#259](https://github.com/Microsoft/vscode-python/issues/259)) -* Automatic activation of virtual or conda environment in terminal when executing Python code/file ([#383](https://github.com/Microsoft/vscode-python/issues/383)) -* Introduce a `Python: Create Terminal` to create a terminal that activates the selected virtual/conda environment ([#622](https://github.com/Microsoft/vscode-python/issues/622)) -* Add a `ko-kr` translation ([#540](https://github.com/Microsoft/vscode-python/pull/540)) -* Add a `ru` translation ([#411](https://github.com/Microsoft/vscode-python/pull/411)) -* Performance improvements to detection of virtual environments in current workspace ([#372](https://github.com/Microsoft/vscode-python/issues/372)) -* Correctly detect 64-bit python ([#414](https://github.com/Microsoft/vscode-python/issues/414)) -* Display parameter information while typing ([#70](https://github.com/Microsoft/vscode-python/issues/70)) -* Use `localhost` instead of `0.0.0.0` when starting debug servers ([#205](https://github.com/Microsoft/vscode-python/issues/205)) -* Ability to configure host name of debug server ([#227](https://github.com/Microsoft/vscode-python/issues/227)) -* Use environment variable PYTHONPATH defined in `.env` for intellisense and code navigation ([#316](https://github.com/Microsoft/vscode-python/issues/316)) -* Support path variable when debugging ([#436](https://github.com/Microsoft/vscode-python/issues/436)) -* Ensure virtual environments can be created in `.env` directory ([#435](https://github.com/Microsoft/vscode-python/issues/435), [#482](https://github.com/Microsoft/vscode-python/issues/482), [#486](https://github.com/Microsoft/vscode-python/issues/486)) -* Reload environment variables from `.env` without having to restart VS Code ([#183](https://github.com/Microsoft/vscode-python/issues/183)) -* Support debugging of Pyramid framework on Windows ([#519](https://github.com/Microsoft/vscode-python/issues/519)) -* Code snippet for `pubd` ([#545](https://github.com/Microsoft/vscode-python/issues/545)) -* Code clean up ([#353](https://github.com/Microsoft/vscode-python/issues/353), [#352](https://github.com/Microsoft/vscode-python/issues/352), [#354](https://github.com/Microsoft/vscode-python/issues/354), [#456](https://github.com/Microsoft/vscode-python/issues/456), [#491](https://github.com/Microsoft/vscode-python/issues/491), [#228](https://github.com/Microsoft/vscode-python/issues/228), [#549](https://github.com/Microsoft/vscode-python/issues/545), [#594](https://github.com/Microsoft/vscode-python/issues/594), [#617](https://github.com/Microsoft/vscode-python/issues/617), [#556](https://github.com/Microsoft/vscode-python/issues/556)) -* Move to `yarn` from `npm` ([#421](https://github.com/Microsoft/vscode-python/issues/421)) -* Add code coverage for extension itself ([#464](https://github.com/Microsoft/vscode-python/issues/464)) -* Releasing [insiders build](https://pvsc.blob.core.windows.net/extension-builds/ms-python-insiders.vsix) of the extension and uploading to cloud storage ([#429](https://github.com/Microsoft/vscode-python/issues/429)) -* Japanese translation ([#434](https://github.com/Microsoft/vscode-python/pull/434)) -* Russian translation ([#411](https://github.com/Microsoft/vscode-python/pull/411)) -* Support paths with spaces when generating tags with `Build Workspace Symbols` ([#44](https://github.com/Microsoft/vscode-python/issues/44)) -* Add ability to configure the linters ([#572](https://github.com/Microsoft/vscode-python/issues/572)) -* Add default set of rules for Pylint ([#554](https://github.com/Microsoft/vscode-python/issues/554)) -* Prompt to install formatter if not available ([#524](https://github.com/Microsoft/vscode-python/issues/524)) -* work around `editor.formatOnSave` failing when taking more then 750ms ([#124](https://github.com/Microsoft/vscode-python/issues/124), [#590](https://github.com/Microsoft/vscode-python/issues/590), [#624](https://github.com/Microsoft/vscode-python/issues/624), [#427](https://github.com/Microsoft/vscode-python/issues/427), [#492](https://github.com/Microsoft/vscode-python/issues/492)) -* Function argument completion no longer automatically includes the default argument ([#522](https://github.com/Microsoft/vscode-python/issues/522)) -* When sending a selection to the terminal, keep the focus in the editor window ([#60](https://github.com/Microsoft/vscode-python/issues/60)) -* Install packages for non-environment Pythons as `--user` installs ([#527](https://github.com/Microsoft/vscode-python/issues/527)) -* No longer suggest the system Python install on macOS when running `Select Interpreter` as it's too outdated (e.g. lacks `pip`) ([#440](https://github.com/Microsoft/vscode-python/issues/440)) -* Fix potential hang from Intellisense ([#423](https://github.com/Microsoft/vscode-python/issues/423)) +- Support cached interpreter locations for faster interpreter selection ([#666](https://github.com/Microsoft/vscode-python/issues/259)) +- Sending a block of code with multiple global-level scopes now works ([#259](https://github.com/Microsoft/vscode-python/issues/259)) +- Automatic activation of virtual or conda environment in terminal when executing Python code/file ([#383](https://github.com/Microsoft/vscode-python/issues/383)) +- Introduce a `Python: Create Terminal` to create a terminal that activates the selected virtual/conda environment ([#622](https://github.com/Microsoft/vscode-python/issues/622)) +- Add a `ko-kr` translation ([#540](https://github.com/Microsoft/vscode-python/pull/540)) +- Add a `ru` translation ([#411](https://github.com/Microsoft/vscode-python/pull/411)) +- Performance improvements to detection of virtual environments in current workspace ([#372](https://github.com/Microsoft/vscode-python/issues/372)) +- Correctly detect 64-bit python ([#414](https://github.com/Microsoft/vscode-python/issues/414)) +- Display parameter information while typing ([#70](https://github.com/Microsoft/vscode-python/issues/70)) +- Use `localhost` instead of `0.0.0.0` when starting debug servers ([#205](https://github.com/Microsoft/vscode-python/issues/205)) +- Ability to configure host name of debug server ([#227](https://github.com/Microsoft/vscode-python/issues/227)) +- Use environment variable PYTHONPATH defined in `.env` for intellisense and code navigation ([#316](https://github.com/Microsoft/vscode-python/issues/316)) +- Support path variable when debugging ([#436](https://github.com/Microsoft/vscode-python/issues/436)) +- Ensure virtual environments can be created in `.env` directory ([#435](https://github.com/Microsoft/vscode-python/issues/435), [#482](https://github.com/Microsoft/vscode-python/issues/482), [#486](https://github.com/Microsoft/vscode-python/issues/486)) +- Reload environment variables from `.env` without having to restart VS Code ([#183](https://github.com/Microsoft/vscode-python/issues/183)) +- Support debugging of Pyramid framework on Windows ([#519](https://github.com/Microsoft/vscode-python/issues/519)) +- Code snippet for `pubd` ([#545](https://github.com/Microsoft/vscode-python/issues/545)) +- Code clean up ([#353](https://github.com/Microsoft/vscode-python/issues/353), [#352](https://github.com/Microsoft/vscode-python/issues/352), [#354](https://github.com/Microsoft/vscode-python/issues/354), [#456](https://github.com/Microsoft/vscode-python/issues/456), [#491](https://github.com/Microsoft/vscode-python/issues/491), [#228](https://github.com/Microsoft/vscode-python/issues/228), [#549](https://github.com/Microsoft/vscode-python/issues/545), [#594](https://github.com/Microsoft/vscode-python/issues/594), [#617](https://github.com/Microsoft/vscode-python/issues/617), [#556](https://github.com/Microsoft/vscode-python/issues/556)) +- Move to `yarn` from `npm` ([#421](https://github.com/Microsoft/vscode-python/issues/421)) +- Add code coverage for extension itself ([#464](https://github.com/Microsoft/vscode-python/issues/464)) +- Releasing [insiders build](https://pvsc.blob.core.windows.net/extension-builds/ms-python-insiders.vsix) of the extension and uploading to cloud storage ([#429](https://github.com/Microsoft/vscode-python/issues/429)) +- Japanese translation ([#434](https://github.com/Microsoft/vscode-python/pull/434)) +- Russian translation ([#411](https://github.com/Microsoft/vscode-python/pull/411)) +- Support paths with spaces when generating tags with `Build Workspace Symbols` ([#44](https://github.com/Microsoft/vscode-python/issues/44)) +- Add ability to configure the linters ([#572](https://github.com/Microsoft/vscode-python/issues/572)) +- Add default set of rules for Pylint ([#554](https://github.com/Microsoft/vscode-python/issues/554)) +- Prompt to install formatter if not available ([#524](https://github.com/Microsoft/vscode-python/issues/524)) +- work around `editor.formatOnSave` failing when taking more then 750ms ([#124](https://github.com/Microsoft/vscode-python/issues/124), [#590](https://github.com/Microsoft/vscode-python/issues/590), [#624](https://github.com/Microsoft/vscode-python/issues/624), [#427](https://github.com/Microsoft/vscode-python/issues/427), [#492](https://github.com/Microsoft/vscode-python/issues/492)) +- Function argument completion no longer automatically includes the default argument ([#522](https://github.com/Microsoft/vscode-python/issues/522)) +- When sending a selection to the terminal, keep the focus in the editor window ([#60](https://github.com/Microsoft/vscode-python/issues/60)) +- Install packages for non-environment Pythons as `--user` installs ([#527](https://github.com/Microsoft/vscode-python/issues/527)) +- No longer suggest the system Python install on macOS when running `Select Interpreter` as it's too outdated (e.g. lacks `pip`) ([#440](https://github.com/Microsoft/vscode-python/issues/440)) +- Fix potential hang from Intellisense ([#423](https://github.com/Microsoft/vscode-python/issues/423)) ## Version 0.9.1 (19 December 2017) -* Fixes the compatibility issue with the [Visual Studio Code Tools for AI](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.vscode-ai) [#432](https://github.com/Microsoft/vscode-python/issues/432) -* Display runtime errors encountered when running a python program without debugging [#454](https://github.com/Microsoft/vscode-python/issues/454) +- Fixes the compatibility issue with the [Visual Studio Code Tools for AI](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.vscode-ai) [#432](https://github.com/Microsoft/vscode-python/issues/432) +- Display runtime errors encountered when running a python program without debugging [#454](https://github.com/Microsoft/vscode-python/issues/454) ## Version 0.9.0 (14 December 2017) -* Translated the commands to simplified Chinese [#240](https://github.com/Microsoft/vscode-python/pull/240) (thanks [Wai Sui kei](https://github.com/WaiSiuKei)) -* Change all links to point to their Python 3 equivalents instead of Python 2[#203](https://github.com/Microsoft/vscode-python/issues/203) -* Respect `{workspaceFolder}` [#258](https://github.com/Microsoft/vscode-python/issues/258) -* Running a program using Ctrl-F5 will work more than once [#25](https://github.com/Microsoft/vscode-python/issues/25) -* Removed the feedback service to rely on VS Code's own support (which fixed an issue of document reformatting failing) [#245](https://github.com/Microsoft/vscode-python/issues/245), [#303](https://github.com/Microsoft/vscode-python/issues/303), [#363](https://github.com/Microsoft/vscode-python/issues/365) -* Do not create empty '.vscode' directory [#253](https://github.com/Microsoft/vscode-python/issues/253), [#277](https://github.com/Microsoft/vscode-python/issues/277) -* Ensure python execution environment handles unicode characters [#393](https://github.com/Microsoft/vscode-python/issues/393) -* Remove Jupyter support in favour of the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=donjayamanne.jupyter) [#223](https://github.com/microsoft/vscode-python/issues/223) +- Translated the commands to simplified Chinese [#240](https://github.com/Microsoft/vscode-python/pull/240) (thanks [Wai Sui kei](https://github.com/WaiSiuKei)) +- Change all links to point to their Python 3 equivalents instead of Python 2[#203](https://github.com/Microsoft/vscode-python/issues/203) +- Respect `{workspaceFolder}` [#258](https://github.com/Microsoft/vscode-python/issues/258) +- Running a program using Ctrl-F5 will work more than once [#25](https://github.com/Microsoft/vscode-python/issues/25) +- Removed the feedback service to rely on VS Code's own support (which fixed an issue of document reformatting failing) [#245](https://github.com/Microsoft/vscode-python/issues/245), [#303](https://github.com/Microsoft/vscode-python/issues/303), [#363](https://github.com/Microsoft/vscode-python/issues/365) +- Do not create empty '.vscode' directory [#253](https://github.com/Microsoft/vscode-python/issues/253), [#277](https://github.com/Microsoft/vscode-python/issues/277) +- Ensure python execution environment handles unicode characters [#393](https://github.com/Microsoft/vscode-python/issues/393) +- Remove Jupyter support in favour of the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=donjayamanne.jupyter) [#223](https://github.com/microsoft/vscode-python/issues/223) ### `conda` -* Support installing Pylint using conda or pip when an Anaconda installation of Python is selected as the active interpreter [#301](https://github.com/Microsoft/vscode-python/issues/301) -* Add JSON schema support for conda's meta.yaml [#281](https://github.com/Microsoft/vscode-python/issues/281) -* Add JSON schema support for conda's environment.yml [#280](https://github.com/Microsoft/vscode-python/issues/280) -* Add JSON schema support for .condarc [#189](https://github.com/Microsoft/vscode-python/issues/280) -* Ensure company name 'Continuum Analytics' is replaced with 'Ananconda Inc' in the list of interpreters [#390](https://github.com/Microsoft/vscode-python/issues/390) -* Display the version of the interpreter instead of conda [#378](https://github.com/Microsoft/vscode-python/issues/378) -* Detect Anaconda on Linux even if it is not in the current path [#22](https://github.com/Microsoft/vscode-python/issues/22) +- Support installing Pylint using conda or pip when an Anaconda installation of Python is selected as the active interpreter [#301](https://github.com/Microsoft/vscode-python/issues/301) +- Add JSON schema support for conda's meta.yaml [#281](https://github.com/Microsoft/vscode-python/issues/281) +- Add JSON schema support for conda's environment.yml [#280](https://github.com/Microsoft/vscode-python/issues/280) +- Add JSON schema support for .condarc [#189](https://github.com/Microsoft/vscode-python/issues/280) +- Ensure company name 'Continuum Analytics' is replaced with 'Ananconda Inc' in the list of interpreters [#390](https://github.com/Microsoft/vscode-python/issues/390) +- Display the version of the interpreter instead of conda [#378](https://github.com/Microsoft/vscode-python/issues/378) +- Detect Anaconda on Linux even if it is not in the current path [#22](https://github.com/Microsoft/vscode-python/issues/22) ### Interpreter selection -* Fixes in the discovery and display of interpreters, including virtual environments [#56](https://github.com/Microsoft/vscode-python/issues/56) -* Retrieve the right value from the registry when determining the version of an interpreter on Windows [#389](https://github.com/Microsoft/vscode-python/issues/389) +- Fixes in the discovery and display of interpreters, including virtual environments [#56](https://github.com/Microsoft/vscode-python/issues/56) +- Retrieve the right value from the registry when determining the version of an interpreter on Windows [#389](https://github.com/Microsoft/vscode-python/issues/389) ### Intellisense -* Fetch intellisense details on-demand instead of for all possible completions [#152](https://github.com/Microsoft/vscode-python/issues/152) -* Disable auto completion in comments and strings [#110](https://github.com/Microsoft/vscode-python/issues/110), [#921](https://github.com/Microsoft/vscode-python/issues/921), [#34](https://github.com/Microsoft/vscode-python/issues/34) +- Fetch intellisense details on-demand instead of for all possible completions [#152](https://github.com/Microsoft/vscode-python/issues/152) +- Disable auto completion in comments and strings [#110](https://github.com/Microsoft/vscode-python/issues/110), [#921](https://github.com/Microsoft/vscode-python/issues/921), [#34](https://github.com/Microsoft/vscode-python/issues/34) ### Linting -* Deprecate `python.linting.lintOnTextChange` [#313](https://github.com/Microsoft/vscode-python/issues/313), [#297](https://github.com/Microsoft/vscode-python/issues/297), [#28](https://github.com/Microsoft/vscode-python/issues/28), [#272](https://github.com/Microsoft/vscode-python/issues/272) -* Refactor code for executing linters (fixes running the proper linter under the selected interpreter) [#351](https://github.com/Microsoft/vscode-python/issues/351), [#397](https://github.com/Microsoft/vscode-python/issues/397) -* Don't attempt to install linters when not in a workspace [#42](https://github.com/Microsoft/vscode-python/issues/42) -* Honour `python.linting.enabled` [#26](https://github.com/Microsoft/vscode-python/issues/26) -* Don't display message 'Linter pylint is not installed' when changing settings [#260](https://github.com/Microsoft/vscode-python/issues/260) -* Display a meaningful message if pip is unavailable to install necessary module such as 'pylint' [#266](https://github.com/Microsoft/vscode-python/issues/266) -* Improvement environment variable parsing in the debugging (allows for embedded `=`) [#149](https://github.com/Microsoft/vscode-python/issues/149), [#361](https://github.com/Microsoft/vscode-python/issues/361) +- Deprecate `python.linting.lintOnTextChange` [#313](https://github.com/Microsoft/vscode-python/issues/313), [#297](https://github.com/Microsoft/vscode-python/issues/297), [#28](https://github.com/Microsoft/vscode-python/issues/28), [#272](https://github.com/Microsoft/vscode-python/issues/272) +- Refactor code for executing linters (fixes running the proper linter under the selected interpreter) [#351](https://github.com/Microsoft/vscode-python/issues/351), [#397](https://github.com/Microsoft/vscode-python/issues/397) +- Don't attempt to install linters when not in a workspace [#42](https://github.com/Microsoft/vscode-python/issues/42) +- Honour `python.linting.enabled` [#26](https://github.com/Microsoft/vscode-python/issues/26) +- Don't display message 'Linter pylint is not installed' when changing settings [#260](https://github.com/Microsoft/vscode-python/issues/260) +- Display a meaningful message if pip is unavailable to install necessary module such as 'pylint' [#266](https://github.com/Microsoft/vscode-python/issues/266) +- Improvement environment variable parsing in the debugging (allows for embedded `=`) [#149](https://github.com/Microsoft/vscode-python/issues/149), [#361](https://github.com/Microsoft/vscode-python/issues/361) ### Debugging -* Improve selecting the port used when debugging [#304](https://github.com/Microsoft/vscode-python/pull/304) -* Don't block debugging in other extensions [#58](https://github.com/Microsoft/vscode-python/issues/58) -* Don't trigger an error to the Console Window when trying to debug an invalid Python file [#157](https://github.com/Microsoft/vscode-python/issues/157) -* No longer prompt to `Press any key to continue . . .` once debugging finishes [#239](https://github.com/Microsoft/vscode-python/issues/239) -* Do not start the extension when debugging non-Python projects [#57](https://github.com/Microsoft/vscode-python/issues/57) -* Support custom external terminals in debugger [#250](https://github.com/Microsoft/vscode-python/issues/250), [#114](https://github.com/Microsoft/vscode-python/issues/114) -* Debugging a python program should not display the message 'Cannot read property …' [#247](https://github.com/Microsoft/vscode-python/issues/247) +- Improve selecting the port used when debugging [#304](https://github.com/Microsoft/vscode-python/pull/304) +- Don't block debugging in other extensions [#58](https://github.com/Microsoft/vscode-python/issues/58) +- Don't trigger an error to the Console Window when trying to debug an invalid Python file [#157](https://github.com/Microsoft/vscode-python/issues/157) +- No longer prompt to `Press any key to continue . . .` once debugging finishes [#239](https://github.com/Microsoft/vscode-python/issues/239) +- Do not start the extension when debugging non-Python projects [#57](https://github.com/Microsoft/vscode-python/issues/57) +- Support custom external terminals in debugger [#250](https://github.com/Microsoft/vscode-python/issues/250), [#114](https://github.com/Microsoft/vscode-python/issues/114) +- Debugging a python program should not display the message 'Cannot read property …' [#247](https://github.com/Microsoft/vscode-python/issues/247) ### Testing -* Refactor unit test library execution code [#350](https://github.com/Microsoft/vscode-python/issues/350) +- Refactor unit test library execution code [#350](https://github.com/Microsoft/vscode-python/issues/350) ### Formatting -* Deprecate the setting `python.formatting.formatOnSave` with an appropriate message [#285](https://github.com/Microsoft/vscode-python/issues/285), [#309](https://github.com/Microsoft/vscode-python/issues/309) +- Deprecate the setting `python.formatting.formatOnSave` with an appropriate message [#285](https://github.com/Microsoft/vscode-python/issues/285), [#309](https://github.com/Microsoft/vscode-python/issues/309) ## Version 0.8.0 (9 November 2017) -* Add support for multi-root workspaces [#1228](https://github.com/DonJayamanne/pythonVSCode/issues/1228), [#1302](https://github.com/DonJayamanne/pythonVSCode/pull/1302), [#1328](https://github.com/DonJayamanne/pythonVSCode/issues/1328), [#1357](https://github.com/DonJayamanne/pythonVSCode/pull/1357) -* Add code snippet for ```ipdb``` [#1141](https://github.com/DonJayamanne/pythonVSCode/pull/1141) -* Add ability to resolving environment variables in path to ```mypy``` [#1195](https://github.com/DonJayamanne/pythonVSCode/issues/1195) -* Add ability to disable a linter globally and disable prompts to install linters [#1207](https://github.com/DonJayamanne/pythonVSCode/issues/1207) -* Auto-selecting an interpreter from a virtual environment if only one is found in the root directory of the project [#1216](https://github.com/DonJayamanne/pythonVSCode/issues/1216) -* Add support for specifying the working directory for unit tests [#1155](https://github.com/DonJayamanne/pythonVSCode/issues/1155), [#1185](https://github.com/DonJayamanne/pythonVSCode/issues/1185) -* Add syntax highlighting of pip requirements files [#1247](https://github.com/DonJayamanne/pythonVSCode/pull/1247) -* Add ability to select an interpreter even when a workspace is not open [#1260](https://github.com/DonJayamanne/pythonVSCode/issues/1260), [#1263](https://github.com/DonJayamanne/pythonVSCode/pull/1263) -* Display a code lens to change the selected interpreter to the one specified in the shebang line [#1257](https://github.com/DonJayamanne/pythonVSCode/pull/1257), [#1263](https://github.com/DonJayamanne/pythonVSCode/pull/1263), [#1267](https://github.com/DonJayamanne/pythonVSCode/pull/1267), [#1280](https://github.com/DonJayamanne/pythonVSCode/issues/1280), [#1261](https://github.com/DonJayamanne/pythonVSCode/issues/1261), [#1290](https://github.com/DonJayamanne/pythonVSCode/pull/1290) -* Expand list of interpreters displayed for selection [#1147](https://github.com/DonJayamanne/pythonVSCode/issues/1147), [#1148](https://github.com/DonJayamanne/pythonVSCode/issues/1148), [#1224](https://github.com/DonJayamanne/pythonVSCode/pull/1224), [#1240](https://github.com/DonJayamanne/pythonVSCode/pull/1240) -* Display details of current or selected interpreter in statusbar [#1147](https://github.com/DonJayamanne/pythonVSCode/issues/1147), [#1217](https://github.com/DonJayamanne/pythonVSCode/issues/1217) -* Ensure paths in workspace symbols are not prefixed with ```.vscode``` [#816](https://github.com/DonJayamanne/pythonVSCode/issues/816), [#1066](https://github.com/DonJayamanne/pythonVSCode/pull/1066), [#829](https://github.com/DonJayamanne/pythonVSCode/issues/829) -* Ensure paths in ```PYTHONPATH``` environment variable are delimited using the OS-specific path delimiter [#832](https://github.com/DonJayamanne/pythonVSCode/issues/832) -* Ensure ```Rope``` is not packaged with the extension [#1208](https://github.com/DonJayamanne/pythonVSCode/issues/1208), [#1207](https://github.com/DonJayamanne/pythonVSCode/issues/1207), [#1243](https://github.com/DonJayamanne/pythonVSCode/pull/1243), [#1229](https://github.com/DonJayamanne/pythonVSCode/issues/1229) -* Ensure ctags are rebuilt as expected upon file save [#624](https://github.com/DonJayamanne/pythonVSCode/issues/1212) -* Ensure right test method is executed when two test methods exist with the same name in different classes [#1203](https://github.com/DonJayamanne/pythonVSCode/issues/1203) -* Ensure unit tests run successfully on Travis for both Python 2.7 and 3.6 [#1255](https://github.com/DonJayamanne/pythonVSCode/pull/1255), [#1241](https://github.com/DonJayamanne/pythonVSCode/issues/1241), [#1315](https://github.com/DonJayamanne/pythonVSCode/issues/1315) -* Fix building of ctags when a path contains a space [#1064](https://github.com/DonJayamanne/pythonVSCode/issues/1064), [#1144](https://github.com/DonJayamanne/pythonVSCode/issues/1144),, [#1213](https://github.com/DonJayamanne/pythonVSCode/pull/1213) -* Fix autocompletion in unsaved Python files [#1194](https://github.com/DonJayamanne/pythonVSCode/issues/1194) -* Fix running of test methods in nose [#597](https://github.com/DonJayamanne/pythonVSCode/issues/597), [#1225](https://github.com/DonJayamanne/pythonVSCode/pull/1225) -* Fix to disable linting of diff windows [#1221](https://github.com/DonJayamanne/pythonVSCode/issues/1221), [#1244](https://github.com/DonJayamanne/pythonVSCode/pull/1244) -* Fix docstring formatting [#1188](https://github.com/DonJayamanne/pythonVSCode/issues/1188) -* Fix to ensure language features can run in parallel without interference with one another [#1314](https://github.com/DonJayamanne/pythonVSCode/issues/1314), [#1318](https://github.com/DonJayamanne/pythonVSCode/pull/1318) -* Fix to ensure unit tests can be debugged more than once per run [#948](https://github.com/DonJayamanne/pythonVSCode/issues/948), [#1353](https://github.com/DonJayamanne/pythonVSCode/pull/1353) -* Fix to ensure parameterized unit tests can be debugged [#1284](https://github.com/DonJayamanne/pythonVSCode/issues/1284), [#1299](https://github.com/DonJayamanne/pythonVSCode/pull/1299) -* Fix issue that causes debugger to freeze/hang [#1041](https://github.com/DonJayamanne/pythonVSCode/issues/1041), [#1354](https://github.com/DonJayamanne/pythonVSCode/pull/1354) -* Fix to support unicode characters in Python tests [#1282](https://github.com/DonJayamanne/pythonVSCode/issues/1282), [#1291](https://github.com/DonJayamanne/pythonVSCode/pull/1291) -* Changes as a result of VS Code API changes [#1270](https://github.com/DonJayamanne/pythonVSCode/issues/1270), [#1288](https://github.com/DonJayamanne/pythonVSCode/pull/1288), [#1372](https://github.com/DonJayamanne/pythonVSCode/issues/1372), [#1300](https://github.com/DonJayamanne/pythonVSCode/pull/1300), [#1298](https://github.com/DonJayamanne/pythonVSCode/issues/1298) -* Updates to Readme [#1212](https://github.com/DonJayamanne/pythonVSCode/issues/1212), [#1222](https://github.com/DonJayamanne/pythonVSCode/issues/1222) -* Fix executing a command under PowerShell [#1098](https://github.com/DonJayamanne/pythonVSCode/issues/1098) +- Add support for multi-root workspaces [#1228](https://github.com/DonJayamanne/pythonVSCode/issues/1228), [#1302](https://github.com/DonJayamanne/pythonVSCode/pull/1302), [#1328](https://github.com/DonJayamanne/pythonVSCode/issues/1328), [#1357](https://github.com/DonJayamanne/pythonVSCode/pull/1357) +- Add code snippet for `ipdb` [#1141](https://github.com/DonJayamanne/pythonVSCode/pull/1141) +- Add ability to resolving environment variables in path to `mypy` [#1195](https://github.com/DonJayamanne/pythonVSCode/issues/1195) +- Add ability to disable a linter globally and disable prompts to install linters [#1207](https://github.com/DonJayamanne/pythonVSCode/issues/1207) +- Auto-selecting an interpreter from a virtual environment if only one is found in the root directory of the project [#1216](https://github.com/DonJayamanne/pythonVSCode/issues/1216) +- Add support for specifying the working directory for unit tests [#1155](https://github.com/DonJayamanne/pythonVSCode/issues/1155), [#1185](https://github.com/DonJayamanne/pythonVSCode/issues/1185) +- Add syntax highlighting of pip requirements files [#1247](https://github.com/DonJayamanne/pythonVSCode/pull/1247) +- Add ability to select an interpreter even when a workspace is not open [#1260](https://github.com/DonJayamanne/pythonVSCode/issues/1260), [#1263](https://github.com/DonJayamanne/pythonVSCode/pull/1263) +- Display a code lens to change the selected interpreter to the one specified in the shebang line [#1257](https://github.com/DonJayamanne/pythonVSCode/pull/1257), [#1263](https://github.com/DonJayamanne/pythonVSCode/pull/1263), [#1267](https://github.com/DonJayamanne/pythonVSCode/pull/1267), [#1280](https://github.com/DonJayamanne/pythonVSCode/issues/1280), [#1261](https://github.com/DonJayamanne/pythonVSCode/issues/1261), [#1290](https://github.com/DonJayamanne/pythonVSCode/pull/1290) +- Expand list of interpreters displayed for selection [#1147](https://github.com/DonJayamanne/pythonVSCode/issues/1147), [#1148](https://github.com/DonJayamanne/pythonVSCode/issues/1148), [#1224](https://github.com/DonJayamanne/pythonVSCode/pull/1224), [#1240](https://github.com/DonJayamanne/pythonVSCode/pull/1240) +- Display details of current or selected interpreter in statusbar [#1147](https://github.com/DonJayamanne/pythonVSCode/issues/1147), [#1217](https://github.com/DonJayamanne/pythonVSCode/issues/1217) +- Ensure paths in workspace symbols are not prefixed with `.vscode` [#816](https://github.com/DonJayamanne/pythonVSCode/issues/816), [#1066](https://github.com/DonJayamanne/pythonVSCode/pull/1066), [#829](https://github.com/DonJayamanne/pythonVSCode/issues/829) +- Ensure paths in `PYTHONPATH` environment variable are delimited using the OS-specific path delimiter [#832](https://github.com/DonJayamanne/pythonVSCode/issues/832) +- Ensure `Rope` is not packaged with the extension [#1208](https://github.com/DonJayamanne/pythonVSCode/issues/1208), [#1207](https://github.com/DonJayamanne/pythonVSCode/issues/1207), [#1243](https://github.com/DonJayamanne/pythonVSCode/pull/1243), [#1229](https://github.com/DonJayamanne/pythonVSCode/issues/1229) +- Ensure ctags are rebuilt as expected upon file save [#624](https://github.com/DonJayamanne/pythonVSCode/issues/1212) +- Ensure right test method is executed when two test methods exist with the same name in different classes [#1203](https://github.com/DonJayamanne/pythonVSCode/issues/1203) +- Ensure unit tests run successfully on Travis for both Python 2.7 and 3.6 [#1255](https://github.com/DonJayamanne/pythonVSCode/pull/1255), [#1241](https://github.com/DonJayamanne/pythonVSCode/issues/1241), [#1315](https://github.com/DonJayamanne/pythonVSCode/issues/1315) +- Fix building of ctags when a path contains a space [#1064](https://github.com/DonJayamanne/pythonVSCode/issues/1064), [#1144](https://github.com/DonJayamanne/pythonVSCode/issues/1144),, [#1213](https://github.com/DonJayamanne/pythonVSCode/pull/1213) +- Fix autocompletion in unsaved Python files [#1194](https://github.com/DonJayamanne/pythonVSCode/issues/1194) +- Fix running of test methods in nose [#597](https://github.com/DonJayamanne/pythonVSCode/issues/597), [#1225](https://github.com/DonJayamanne/pythonVSCode/pull/1225) +- Fix to disable linting of diff windows [#1221](https://github.com/DonJayamanne/pythonVSCode/issues/1221), [#1244](https://github.com/DonJayamanne/pythonVSCode/pull/1244) +- Fix docstring formatting [#1188](https://github.com/DonJayamanne/pythonVSCode/issues/1188) +- Fix to ensure language features can run in parallel without interference with one another [#1314](https://github.com/DonJayamanne/pythonVSCode/issues/1314), [#1318](https://github.com/DonJayamanne/pythonVSCode/pull/1318) +- Fix to ensure unit tests can be debugged more than once per run [#948](https://github.com/DonJayamanne/pythonVSCode/issues/948), [#1353](https://github.com/DonJayamanne/pythonVSCode/pull/1353) +- Fix to ensure parameterized unit tests can be debugged [#1284](https://github.com/DonJayamanne/pythonVSCode/issues/1284), [#1299](https://github.com/DonJayamanne/pythonVSCode/pull/1299) +- Fix issue that causes debugger to freeze/hang [#1041](https://github.com/DonJayamanne/pythonVSCode/issues/1041), [#1354](https://github.com/DonJayamanne/pythonVSCode/pull/1354) +- Fix to support unicode characters in Python tests [#1282](https://github.com/DonJayamanne/pythonVSCode/issues/1282), [#1291](https://github.com/DonJayamanne/pythonVSCode/pull/1291) +- Changes as a result of VS Code API changes [#1270](https://github.com/DonJayamanne/pythonVSCode/issues/1270), [#1288](https://github.com/DonJayamanne/pythonVSCode/pull/1288), [#1372](https://github.com/DonJayamanne/pythonVSCode/issues/1372), [#1300](https://github.com/DonJayamanne/pythonVSCode/pull/1300), [#1298](https://github.com/DonJayamanne/pythonVSCode/issues/1298) +- Updates to Readme [#1212](https://github.com/DonJayamanne/pythonVSCode/issues/1212), [#1222](https://github.com/DonJayamanne/pythonVSCode/issues/1222) +- Fix executing a command under PowerShell [#1098](https://github.com/DonJayamanne/pythonVSCode/issues/1098) ## Version 0.7.0 (3 August 2017) -* Displaying internal documentation [#1008](https://github.com/DonJayamanne/pythonVSCode/issues/1008), [#10860](https://github.com/DonJayamanne/pythonVSCode/issues/10860) -* Fixes to 'async with' snippet [#1108](https://github.com/DonJayamanne/pythonVSCode/pull/1108), [#996](https://github.com/DonJayamanne/pythonVSCode/issues/996) -* Add support for environment variable in unit tests [#1074](https://github.com/DonJayamanne/pythonVSCode/issues/1074) -* Fixes to unit test code lenses not being displayed [#1115](https://github.com/DonJayamanne/pythonVSCode/issues/1115) -* Fix to empty brackets being added [#1110](https://github.com/DonJayamanne/pythonVSCode/issues/1110), [#1031](https://github.com/DonJayamanne/pythonVSCode/issues/1031) -* Fix debugging of Django applications [#819](https://github.com/DonJayamanne/pythonVSCode/issues/819), [#999](https://github.com/DonJayamanne/pythonVSCode/issues/999) -* Update isort to the latest version [#1134](https://github.com/DonJayamanne/pythonVSCode/issues/1134), [#1135](https://github.com/DonJayamanne/pythonVSCode/pull/1135) -* Fix issue causing intellisense and similar functionality to stop working [#1072](https://github.com/DonJayamanne/pythonVSCode/issues/1072), [#1118](https://github.com/DonJayamanne/pythonVSCode/pull/1118), [#1089](https://github.com/DonJayamanne/pythonVSCode/issues/1089) -* Bunch of unit tests and code cleanup -* Resolve issue where navigation to decorated function goes to decorator [#742](https://github.com/DonJayamanne/pythonVSCode/issues/742) -* Go to symbol in workspace leads to nonexisting files [#816](https://github.com/DonJayamanne/pythonVSCode/issues/816), [#829](https://github.com/DonJayamanne/pythonVSCode/issues/829) + +- Displaying internal documentation [#1008](https://github.com/DonJayamanne/pythonVSCode/issues/1008), [#10860](https://github.com/DonJayamanne/pythonVSCode/issues/10860) +- Fixes to 'async with' snippet [#1108](https://github.com/DonJayamanne/pythonVSCode/pull/1108), [#996](https://github.com/DonJayamanne/pythonVSCode/issues/996) +- Add support for environment variable in unit tests [#1074](https://github.com/DonJayamanne/pythonVSCode/issues/1074) +- Fixes to unit test code lenses not being displayed [#1115](https://github.com/DonJayamanne/pythonVSCode/issues/1115) +- Fix to empty brackets being added [#1110](https://github.com/DonJayamanne/pythonVSCode/issues/1110), [#1031](https://github.com/DonJayamanne/pythonVSCode/issues/1031) +- Fix debugging of Django applications [#819](https://github.com/DonJayamanne/pythonVSCode/issues/819), [#999](https://github.com/DonJayamanne/pythonVSCode/issues/999) +- Update isort to the latest version [#1134](https://github.com/DonJayamanne/pythonVSCode/issues/1134), [#1135](https://github.com/DonJayamanne/pythonVSCode/pull/1135) +- Fix issue causing intellisense and similar functionality to stop working [#1072](https://github.com/DonJayamanne/pythonVSCode/issues/1072), [#1118](https://github.com/DonJayamanne/pythonVSCode/pull/1118), [#1089](https://github.com/DonJayamanne/pythonVSCode/issues/1089) +- Bunch of unit tests and code cleanup +- Resolve issue where navigation to decorated function goes to decorator [#742](https://github.com/DonJayamanne/pythonVSCode/issues/742) +- Go to symbol in workspace leads to nonexisting files [#816](https://github.com/DonJayamanne/pythonVSCode/issues/816), [#829](https://github.com/DonJayamanne/pythonVSCode/issues/829) ## Version 0.6.9 (22 July 2017) -* Fix to enure custom linter paths are respected [#1106](https://github.com/DonJayamanne/pythonVSCode/issues/1106) + +- Fix to enure custom linter paths are respected [#1106](https://github.com/DonJayamanne/pythonVSCode/issues/1106) ## Version 0.6.8 (20 July 2017) -* Add new editor menu 'Run Current Unit Test File' [#1061](https://github.com/DonJayamanne/pythonVSCode/issues/1061) -* Changed 'mypy-lang' to mypy [#930](https://github.com/DonJayamanne/pythonVSCode/issues/930), [#998](https://github.com/DonJayamanne/pythonVSCode/issues/998), [#505](https://github.com/DonJayamanne/pythonVSCode/issues/505) -* Using "Python -m" to launch linters [#716](https://github.com/DonJayamanne/pythonVSCode/issues/716), [#923](https://github.com/DonJayamanne/pythonVSCode/issues/923), [#1059](https://github.com/DonJayamanne/pythonVSCode/issues/1059) -* Add PEP 526 AutoCompletion [#1102](https://github.com/DonJayamanne/pythonVSCode/pull/1102), [#1101](https://github.com/DonJayamanne/pythonVSCode/issues/1101) -* Resolved issues in Go To and Peek Definitions [#1085](https://github.com/DonJayamanne/pythonVSCode/pull/1085), [#961](https://github.com/DonJayamanne/pythonVSCode/issues/961), [#870](https://github.com/DonJayamanne/pythonVSCode/issues/870) + +- Add new editor menu 'Run Current Unit Test File' [#1061](https://github.com/DonJayamanne/pythonVSCode/issues/1061) +- Changed 'mypy-lang' to mypy [#930](https://github.com/DonJayamanne/pythonVSCode/issues/930), [#998](https://github.com/DonJayamanne/pythonVSCode/issues/998), [#505](https://github.com/DonJayamanne/pythonVSCode/issues/505) +- Using "Python -m" to launch linters [#716](https://github.com/DonJayamanne/pythonVSCode/issues/716), [#923](https://github.com/DonJayamanne/pythonVSCode/issues/923), [#1059](https://github.com/DonJayamanne/pythonVSCode/issues/1059) +- Add PEP 526 AutoCompletion [#1102](https://github.com/DonJayamanne/pythonVSCode/pull/1102), [#1101](https://github.com/DonJayamanne/pythonVSCode/issues/1101) +- Resolved issues in Go To and Peek Definitions [#1085](https://github.com/DonJayamanne/pythonVSCode/pull/1085), [#961](https://github.com/DonJayamanne/pythonVSCode/issues/961), [#870](https://github.com/DonJayamanne/pythonVSCode/issues/870) ## Version 0.6.7 (02 July 2017) -* Updated icon from jpg to png (transparent background) + +- Updated icon from jpg to png (transparent background) ## Version 0.6.6 (02 July 2017) -* Provide details of error with solution for changes to syntax in launch.json [#1047](https://github.com/DonJayamanne/pythonVSCode/issues/1047), [#1025](https://github.com/DonJayamanne/pythonVSCode/issues/1025) -* Provide a warning about known issues with having pyenv.cfg whilst debugging [#913](https://github.com/DonJayamanne/pythonVSCode/issues/913) -* Create .vscode directory if not found [#1043](https://github.com/DonJayamanne/pythonVSCode/issues/1043) -* Highlighted text due to linter errors is off by one column [#965](https://github.com/DonJayamanne/pythonVSCode/issues/965), [#970](https://github.com/DonJayamanne/pythonVSCode/pull/970) -* Added preminary support for WSL Bash and Cygwin [#1049](https://github.com/DonJayamanne/pythonVSCode/pull/1049) -* Ability to configure the linter severity levels [#941](https://github.com/DonJayamanne/pythonVSCode/pull/941), [#895](https://github.com/DonJayamanne/pythonVSCode/issues/895) -* Fixes to unit tests [#1051](https://github.com/DonJayamanne/pythonVSCode/pull/1051), [#1050](https://github.com/DonJayamanne/pythonVSCode/pull/1050) -* Outdent lines following `contibue`, `break` and `return` [#1050](https://github.com/DonJayamanne/pythonVSCode/pull/1050) -* Change location of cache for Jedi files [#1035](https://github.com/DonJayamanne/pythonVSCode/pull/1035) -* Fixes to the way directories are searched for Python interpreters [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569), [#1040](https://github.com/DonJayamanne/pythonVSCode/pull/1040) -* Handle outputs from Python packages that interfere with the way autocompletion is handled [#602](https://github.com/DonJayamanne/pythonVSCode/issues/602) + +- Provide details of error with solution for changes to syntax in launch.json [#1047](https://github.com/DonJayamanne/pythonVSCode/issues/1047), [#1025](https://github.com/DonJayamanne/pythonVSCode/issues/1025) +- Provide a warning about known issues with having pyenv.cfg whilst debugging [#913](https://github.com/DonJayamanne/pythonVSCode/issues/913) +- Create .vscode directory if not found [#1043](https://github.com/DonJayamanne/pythonVSCode/issues/1043) +- Highlighted text due to linter errors is off by one column [#965](https://github.com/DonJayamanne/pythonVSCode/issues/965), [#970](https://github.com/DonJayamanne/pythonVSCode/pull/970) +- Added preliminary support for WSL Bash and Cygwin [#1049](https://github.com/DonJayamanne/pythonVSCode/pull/1049) +- Ability to configure the linter severity levels [#941](https://github.com/DonJayamanne/pythonVSCode/pull/941), [#895](https://github.com/DonJayamanne/pythonVSCode/issues/895) +- Fixes to unit tests [#1051](https://github.com/DonJayamanne/pythonVSCode/pull/1051), [#1050](https://github.com/DonJayamanne/pythonVSCode/pull/1050) +- Outdent lines following `continue`, `break` and `return` [#1050](https://github.com/DonJayamanne/pythonVSCode/pull/1050) +- Change location of cache for Jedi files [#1035](https://github.com/DonJayamanne/pythonVSCode/pull/1035) +- Fixes to the way directories are searched for Python interpreters [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569), [#1040](https://github.com/DonJayamanne/pythonVSCode/pull/1040) +- Handle outputs from Python packages that interfere with the way autocompletion is handled [#602](https://github.com/DonJayamanne/pythonVSCode/issues/602) ## Version 0.6.5 (13 June 2017) -* Fix error in launch.json [#1006](https://github.com/DonJayamanne/pythonVSCode/issues/1006) -* Detect current workspace interpreter when selecting interpreter [#1006](https://github.com/DonJayamanne/pythonVSCode/issues/979) -* Disable output buffering when debugging [#1005](https://github.com/DonJayamanne/pythonVSCode/issues/1005) -* Updated snippets to use correct placeholder syntax [#976](https://github.com/DonJayamanne/pythonVSCode/pull/976) -* Fix hover and auto complete unit tests [#1012](https://github.com/DonJayamanne/pythonVSCode/pull/1012) -* Fix hover definition variable test for Python 3.5 [#1013](https://github.com/DonJayamanne/pythonVSCode/pull/1013) -* Better formatting of docstring [#821](https://github.com/DonJayamanne/pythonVSCode/pull/821), [#919](https://github.com/DonJayamanne/pythonVSCode/pull/919) -* Supporting more paths when searching for Python interpreters [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569) -* Increase buffer output (to support detection large number of tests) [#927](https://github.com/DonJayamanne/pythonVSCode/issues/927) + +- Fix error in launch.json [#1006](https://github.com/DonJayamanne/pythonVSCode/issues/1006) +- Detect current workspace interpreter when selecting interpreter [#1006](https://github.com/DonJayamanne/pythonVSCode/issues/979) +- Disable output buffering when debugging [#1005](https://github.com/DonJayamanne/pythonVSCode/issues/1005) +- Updated snippets to use correct placeholder syntax [#976](https://github.com/DonJayamanne/pythonVSCode/pull/976) +- Fix hover and auto complete unit tests [#1012](https://github.com/DonJayamanne/pythonVSCode/pull/1012) +- Fix hover definition variable test for Python 3.5 [#1013](https://github.com/DonJayamanne/pythonVSCode/pull/1013) +- Better formatting of docstring [#821](https://github.com/DonJayamanne/pythonVSCode/pull/821), [#919](https://github.com/DonJayamanne/pythonVSCode/pull/919) +- Supporting more paths when searching for Python interpreters [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569) +- Increase buffer output (to support detection large number of tests) [#927](https://github.com/DonJayamanne/pythonVSCode/issues/927) ## Version 0.6.4 (4 May 2017) -* Fix dates in changelog [#899](https://github.com/DonJayamanne/pythonVSCode/pull/899) -* Using charriage return or line feeds to split a document into multiple lines [#917](https://github.com/DonJayamanne/pythonVSCode/pull/917), [#821](https://github.com/DonJayamanne/pythonVSCode/issues/821) -* Doc string not being displayed [#888](https://github.com/DonJayamanne/pythonVSCode/issues/888) -* Supporting paths that begin with the ~/ [#909](https://github.com/DonJayamanne/pythonVSCode/issues/909) -* Supporting more paths when searching for Python interpreters [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569) -* Supporting ~/ paths when providing the path to ctag file [#910](https://github.com/DonJayamanne/pythonVSCode/issues/910) -* Disable linting of python files opened in diff viewer [#896](https://github.com/DonJayamanne/pythonVSCode/issues/896) -* Added a new command ```Go to Python Object``` [#928](https://github.com/DonJayamanne/pythonVSCode/issues/928) -* Restored the menu item to rediscover tests [#863](https://github.com/DonJayamanne/pythonVSCode/issues/863) -* Changes to rediscover tests when test files are altered and saved [#863](https://github.com/DonJayamanne/pythonVSCode/issues/863) + +- Fix dates in changelog [#899](https://github.com/DonJayamanne/pythonVSCode/pull/899) +- Using charriage return or line feeds to split a document into multiple lines [#917](https://github.com/DonJayamanne/pythonVSCode/pull/917), [#821](https://github.com/DonJayamanne/pythonVSCode/issues/821) +- Doc string not being displayed [#888](https://github.com/DonJayamanne/pythonVSCode/issues/888) +- Supporting paths that begin with the ~/ [#909](https://github.com/DonJayamanne/pythonVSCode/issues/909) +- Supporting more paths when searching for Python interpreters [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569) +- Supporting ~/ paths when providing the path to ctag file [#910](https://github.com/DonJayamanne/pythonVSCode/issues/910) +- Disable linting of python files opened in diff viewer [#896](https://github.com/DonJayamanne/pythonVSCode/issues/896) +- Added a new command `Go to Python Object` [#928](https://github.com/DonJayamanne/pythonVSCode/issues/928) +- Restored the menu item to rediscover tests [#863](https://github.com/DonJayamanne/pythonVSCode/issues/863) +- Changes to rediscover tests when test files are altered and saved [#863](https://github.com/DonJayamanne/pythonVSCode/issues/863) ## Version 0.6.3 (19 April 2017) -* Fix debugger issue [#893](https://github.com/DonJayamanne/pythonVSCode/issues/893) -* Improvements to debugging unit tests (check if string starts with, instead of comparing equality) [#797](https://github.com/DonJayamanne/pythonVSCode/issues/797) + +- Fix debugger issue [#893](https://github.com/DonJayamanne/pythonVSCode/issues/893) +- Improvements to debugging unit tests (check if string starts with, instead of comparing equality) [#797](https://github.com/DonJayamanne/pythonVSCode/issues/797) ## Version 0.6.2 (13 April 2017) -* Fix incorrect indenting [#880](https://github.com/DonJayamanne/pythonVSCode/issues/880) + +- Fix incorrect indenting [#880](https://github.com/DonJayamanne/pythonVSCode/issues/880) ### Thanks -* [Yuwei Ba](https://github.com/ibigbug) + +- [Yuwei Ba](https://github.com/ibigbug) ## Version 0.6.1 (10 April 2017) -* Add support for new variable syntax in upcoming VS Code release [#774](https://github.com/DonJayamanne/pythonVSCode/issues/774), [#855](https://github.com/DonJayamanne/pythonVSCode/issues/855), [#873](https://github.com/DonJayamanne/pythonVSCode/issues/873), [#823](https://github.com/DonJayamanne/pythonVSCode/issues/823) -* Resolve issues in code refactoring [#802](https://github.com/DonJayamanne/pythonVSCode/issues/802), [#824](https://github.com/DonJayamanne/pythonVSCode/issues/824), [#825](https://github.com/DonJayamanne/pythonVSCode/pull/825) -* Changes to labels in Python Interpreter lookup [#815](https://github.com/DonJayamanne/pythonVSCode/pull/815) -* Resolve Typos [#852](https://github.com/DonJayamanne/pythonVSCode/issues/852) -* Use fully qualitified Python Path when installing dependencies [#866](https://github.com/DonJayamanne/pythonVSCode/issues/866) -* Commands for running tests from a file [#502](https://github.com/DonJayamanne/pythonVSCode/pull/502) -* Fix Sorting of imports when path contains spaces [#811](https://github.com/DonJayamanne/pythonVSCode/issues/811) -* Fixing occasional failure of linters [#793](https://github.com/DonJayamanne/pythonVSCode/issues/793), [#833](https://github.com/DonJayamanne/pythonVSCode/issues/838), [#860](https://github.com/DonJayamanne/pythonVSCode/issues/860) -* Added ability to pre-load some modules to improve autocompletion [#581](https://github.com/DonJayamanne/pythonVSCode/issues/581) + +- Add support for new variable syntax in upcoming VS Code release [#774](https://github.com/DonJayamanne/pythonVSCode/issues/774), [#855](https://github.com/DonJayamanne/pythonVSCode/issues/855), [#873](https://github.com/DonJayamanne/pythonVSCode/issues/873), [#823](https://github.com/DonJayamanne/pythonVSCode/issues/823) +- Resolve issues in code refactoring [#802](https://github.com/DonJayamanne/pythonVSCode/issues/802), [#824](https://github.com/DonJayamanne/pythonVSCode/issues/824), [#825](https://github.com/DonJayamanne/pythonVSCode/pull/825) +- Changes to labels in Python Interpreter lookup [#815](https://github.com/DonJayamanne/pythonVSCode/pull/815) +- Resolve Typos [#852](https://github.com/DonJayamanne/pythonVSCode/issues/852) +- Use fully qualitified Python Path when installing dependencies [#866](https://github.com/DonJayamanne/pythonVSCode/issues/866) +- Commands for running tests from a file [#502](https://github.com/DonJayamanne/pythonVSCode/pull/502) +- Fix Sorting of imports when path contains spaces [#811](https://github.com/DonJayamanne/pythonVSCode/issues/811) +- Fixing occasional failure of linters [#793](https://github.com/DonJayamanne/pythonVSCode/issues/793), [#833](https://github.com/DonJayamanne/pythonVSCode/issues/838), [#860](https://github.com/DonJayamanne/pythonVSCode/issues/860) +- Added ability to pre-load some modules to improve autocompletion [#581](https://github.com/DonJayamanne/pythonVSCode/issues/581) ### Thanks -* [Ashwin Mathews](https://github.com/ajmathews) -* [Alexander Ioannidis](https://github.com/slint) -* [Andreas Schlapsi](https://github.com/aschlapsi) + +- [Ashwin Mathews](https://github.com/ajmathews) +- [Alexander Ioannidis](https://github.com/slint) +- [Andreas Schlapsi](https://github.com/aschlapsi) ## Version 0.6.0 (10 March 2017) -* Moved Jupyter functionality into a separate extension [Jupyter]() -* Updated readme [#779](https://github.com/DonJayamanne/pythonVSCode/issues/779) -* Changing default arguments of ```mypy``` [#658](https://github.com/DonJayamanne/pythonVSCode/issues/658) -* Added ability to disable formatting [#559](https://github.com/DonJayamanne/pythonVSCode/issues/559) -* Fixing ability to run a Python file in a terminal [#784](https://github.com/DonJayamanne/pythonVSCode/issues/784) -* Added support for Proxy settings when installing Python packages using Pip [#778](https://github.com/DonJayamanne/pythonVSCode/issues/778) + +- Moved Jupyter functionality into a separate extension [Jupyter]() +- Updated readme [#779](https://github.com/DonJayamanne/pythonVSCode/issues/779) +- Changing default arguments of `mypy` [#658](https://github.com/DonJayamanne/pythonVSCode/issues/658) +- Added ability to disable formatting [#559](https://github.com/DonJayamanne/pythonVSCode/issues/559) +- Fixing ability to run a Python file in a terminal [#784](https://github.com/DonJayamanne/pythonVSCode/issues/784) +- Added support for Proxy settings when installing Python packages using Pip [#778](https://github.com/DonJayamanne/pythonVSCode/issues/778) ## Version 0.5.9 (3 March 2017) -* Fixed navigating to definitions [#711](https://github.com/DonJayamanne/pythonVSCode/issues/711) -* Support auto detecting binaries from Python Path [#716](https://github.com/DonJayamanne/pythonVSCode/issues/716) -* Setting PYTHONPATH environment variable [#686](https://github.com/DonJayamanne/pythonVSCode/issues/686) -* Improving Linter performance, killing redundant processes [4a8319e](https://github.com/DonJayamanne/pythonVSCode/commit/4a8319e0859f2d49165c9a08fe147a647d03ece9) -* Changed default path of the CATAS file to `.vscode/tags` [#722](https://github.com/DonJayamanne/pythonVSCode/issues/722) -* Add parsing severity level for flake8 and pep8 linters [#709](https://github.com/DonJayamanne/pythonVSCode/pull/709) -* Fix to restore function descriptions (intellisense) [#727](https://github.com/DonJayamanne/pythonVSCode/issues/727) -* Added default configuration for debugging Pyramid [#287](https://github.com/DonJayamanne/pythonVSCode/pull/287) -* Feature request: Run current line in Terminal [#738](https://github.com/DonJayamanne/pythonVSCode/issues/738) -* Miscellaneous improvements to hover provider [6a7a3f3](https://github.com/DonJayamanne/pythonVSCode/commit/6a7a3f32ab8add830d13399fec6f0cdd14cd66fc), [6268306](https://github.com/DonJayamanne/pythonVSCode/commit/62683064d01cfc2b76d9be45587280798a96460b) -* Fixes to rename refactor (due to 'LF' EOL in Windows) [#748](https://github.com/DonJayamanne/pythonVSCode/pull/748) -* Fixes to ctag file being generated in home folder when no workspace is opened [#753](https://github.com/DonJayamanne/pythonVSCode/issues/753) -* Fixes to ctag file being generated in home folder when no workspace is opened [#753](https://github.com/DonJayamanne/pythonVSCode/issues/753) -* Disabling auto-completion in single line comments [#74](https://github.com/DonJayamanne/pythonVSCode/issues/74) -* Fixes to debugging of modules [#518](https://github.com/DonJayamanne/pythonVSCode/issues/518) -* Displaying unit test status icons against unit test code lenses [#678](https://github.com/DonJayamanne/pythonVSCode/issues/678) -* Fix issue where causing 'python.python-debug.startSession' not found message to be displayed when debugging single file [#708](https://github.com/DonJayamanne/pythonVSCode/issues/708) -* Ability to include packages directory when generating tags file [#735](https://github.com/DonJayamanne/pythonVSCode/issues/735) -* Fix issue where running selected text in terminal does not work [#758](https://github.com/DonJayamanne/pythonVSCode/issues/758) -* Fix issue where disabling linter doesn't disable it (when no workspace is open) [#763](https://github.com/DonJayamanne/pythonVSCode/issues/763) -* Search additional directories for Python Interpreters (~/.virtualenvs, ~/Envs, ~/.pyenv) [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569) -* Added ability to pre-load some modules to improve autocompletion [#581](https://github.com/DonJayamanne/pythonVSCode/issues/581) -* Removed invalid default value in launch.json file [#586](https://github.com/DonJayamanne/pythonVSCode/issues/586) -* Added ability to configure the pylint executable path [#766](https://github.com/DonJayamanne/pythonVSCode/issues/766) -* Fixed single file debugger to ensure the Python interpreter configured in python.PythonPath is being used [#769](https://github.com/DonJayamanne/pythonVSCode/issues/769) + +- Fixed navigating to definitions [#711](https://github.com/DonJayamanne/pythonVSCode/issues/711) +- Support auto detecting binaries from Python Path [#716](https://github.com/DonJayamanne/pythonVSCode/issues/716) +- Setting PYTHONPATH environment variable [#686](https://github.com/DonJayamanne/pythonVSCode/issues/686) +- Improving Linter performance, killing redundant processes [4a8319e](https://github.com/DonJayamanne/pythonVSCode/commit/4a8319e0859f2d49165c9a08fe147a647d03ece9) +- Changed default path of the CATAS file to `.vscode/tags` [#722](https://github.com/DonJayamanne/pythonVSCode/issues/722) +- Add parsing severity level for flake8 and pep8 linters [#709](https://github.com/DonJayamanne/pythonVSCode/pull/709) +- Fix to restore function descriptions (intellisense) [#727](https://github.com/DonJayamanne/pythonVSCode/issues/727) +- Added default configuration for debugging Pyramid [#287](https://github.com/DonJayamanne/pythonVSCode/pull/287) +- Feature request: Run current line in Terminal [#738](https://github.com/DonJayamanne/pythonVSCode/issues/738) +- Miscellaneous improvements to hover provider [6a7a3f3](https://github.com/DonJayamanne/pythonVSCode/commit/6a7a3f32ab8add830d13399fec6f0cdd14cd66fc), [6268306](https://github.com/DonJayamanne/pythonVSCode/commit/62683064d01cfc2b76d9be45587280798a96460b) +- Fixes to rename refactor (due to 'LF' EOL in Windows) [#748](https://github.com/DonJayamanne/pythonVSCode/pull/748) +- Fixes to ctag file being generated in home folder when no workspace is opened [#753](https://github.com/DonJayamanne/pythonVSCode/issues/753) +- Fixes to ctag file being generated in home folder when no workspace is opened [#753](https://github.com/DonJayamanne/pythonVSCode/issues/753) +- Disabling auto-completion in single line comments [#74](https://github.com/DonJayamanne/pythonVSCode/issues/74) +- Fixes to debugging of modules [#518](https://github.com/DonJayamanne/pythonVSCode/issues/518) +- Displaying unit test status icons against unit test code lenses [#678](https://github.com/DonJayamanne/pythonVSCode/issues/678) +- Fix issue where causing 'python.python-debug.startSession' not found message to be displayed when debugging single file [#708](https://github.com/DonJayamanne/pythonVSCode/issues/708) +- Ability to include packages directory when generating tags file [#735](https://github.com/DonJayamanne/pythonVSCode/issues/735) +- Fix issue where running selected text in terminal does not work [#758](https://github.com/DonJayamanne/pythonVSCode/issues/758) +- Fix issue where disabling linter doesn't disable it (when no workspace is open) [#763](https://github.com/DonJayamanne/pythonVSCode/issues/763) +- Search additional directories for Python Interpreters (~/.virtualenvs, ~/Envs, ~/.pyenv) [#569](https://github.com/DonJayamanne/pythonVSCode/issues/569) +- Added ability to pre-load some modules to improve autocompletion [#581](https://github.com/DonJayamanne/pythonVSCode/issues/581) +- Removed invalid default value in launch.json file [#586](https://github.com/DonJayamanne/pythonVSCode/issues/586) +- Added ability to configure the pylint executable path [#766](https://github.com/DonJayamanne/pythonVSCode/issues/766) +- Fixed single file debugger to ensure the Python interpreter configured in python.PythonPath is being used [#769](https://github.com/DonJayamanne/pythonVSCode/issues/769) ## Version 0.5.8 (3 February 2017) -* Fixed a bug in [debugging single files without a launch configuration](https://code.visualstudio.com/updates/v1_9#_debugging-without-a-launch-configuration) [#700](https://github.com/DonJayamanne/pythonVSCode/issues/700) -* Fixed error when starting REPL [#692](https://github.com/DonJayamanne/pythonVSCode/issues/692) + +- Fixed a bug in [debugging single files without a launch configuration](https://code.visualstudio.com/updates/v1_9#_debugging-without-a-launch-configuration) [#700](https://github.com/DonJayamanne/pythonVSCode/issues/700) +- Fixed error when starting REPL [#692](https://github.com/DonJayamanne/pythonVSCode/issues/692) ## Version 0.5.7 (3 February 2017) -* Added support for [debugging single files without a launch configuration](https://code.visualstudio.com/updates/v1_9#_debugging-without-a-launch-configuration) -* Adding support for debug snippets [#660](https://github.com/DonJayamanne/pythonVSCode/issues/660) -* Ability to run a selected text in a Django shell [#652](https://github.com/DonJayamanne/pythonVSCode/issues/652) -* Adding support for the use of a customized 'isort' for sorting of imports [#632](https://github.com/DonJayamanne/pythonVSCode/pull/632) -* Debuger auto-detecting python interpreter from the path provided [#688](https://github.com/DonJayamanne/pythonVSCode/issues/688) -* Showing symbol type on hover [#657](https://github.com/DonJayamanne/pythonVSCode/pull/657) -* Fixes to running Python file when terminal uses Powershell [#651](https://github.com/DonJayamanne/pythonVSCode/issues/651) -* Fixes to linter issues when displaying Git diff view for Python files [#665](https://github.com/DonJayamanne/pythonVSCode/issues/665) -* Fixes to 'Go to definition' functionality [#662](https://github.com/DonJayamanne/pythonVSCode/issues/662) -* Fixes to Jupyter cells numbered larger than '10' [#681](https://github.com/DonJayamanne/pythonVSCode/issues/681) + +- Added support for [debugging single files without a launch configuration](https://code.visualstudio.com/updates/v1_9#_debugging-without-a-launch-configuration) +- Adding support for debug snippets [#660](https://github.com/DonJayamanne/pythonVSCode/issues/660) +- Ability to run a selected text in a Django shell [#652](https://github.com/DonJayamanne/pythonVSCode/issues/652) +- Adding support for the use of a customized 'isort' for sorting of imports [#632](https://github.com/DonJayamanne/pythonVSCode/pull/632) +- Debugger auto-detecting python interpreter from the path provided [#688](https://github.com/DonJayamanne/pythonVSCode/issues/688) +- Showing symbol type on hover [#657](https://github.com/DonJayamanne/pythonVSCode/pull/657) +- Fixes to running Python file when terminal uses Powershell [#651](https://github.com/DonJayamanne/pythonVSCode/issues/651) +- Fixes to linter issues when displaying Git diff view for Python files [#665](https://github.com/DonJayamanne/pythonVSCode/issues/665) +- Fixes to 'Go to definition' functionality [#662](https://github.com/DonJayamanne/pythonVSCode/issues/662) +- Fixes to Jupyter cells numbered larger than '10' [#681](https://github.com/DonJayamanne/pythonVSCode/issues/681) ## Version 0.5.6 (16 January 2017) -* Added support for Python 3.6 [#646](https://github.com/DonJayamanne/pythonVSCode/issues/646), [#631](https://github.com/DonJayamanne/pythonVSCode/issues/631), [#619](https://github.com/DonJayamanne/pythonVSCode/issues/619), [#613](https://github.com/DonJayamanne/pythonVSCode/issues/613) -* Autodetect in python path in virtual environments [#353](https://github.com/DonJayamanne/pythonVSCode/issues/353) -* Add syntax highlighting of code samples in hover defintion [#555](https://github.com/DonJayamanne/pythonVSCode/issues/555) -* Launch REPL for currently selected interpreter [#560](https://github.com/DonJayamanne/pythonVSCode/issues/560) -* Fixes to debugging of modules [#589](https://github.com/DonJayamanne/pythonVSCode/issues/589) -* Reminder to install jedi and ctags in Quick Start [#642](https://github.com/DonJayamanne/pythonVSCode/pull/642) -* Improvements to Symbol Provider [#622](https://github.com/DonJayamanne/pythonVSCode/pull/622) -* Changes to disable unit test prompts for workspace [#559](https://github.com/DonJayamanne/pythonVSCode/issues/559) -* Minor fixes [#627](https://github.com/DonJayamanne/pythonVSCode/pull/627) + +- Added support for Python 3.6 [#646](https://github.com/DonJayamanne/pythonVSCode/issues/646), [#631](https://github.com/DonJayamanne/pythonVSCode/issues/631), [#619](https://github.com/DonJayamanne/pythonVSCode/issues/619), [#613](https://github.com/DonJayamanne/pythonVSCode/issues/613) +- Autodetect in python path in virtual environments [#353](https://github.com/DonJayamanne/pythonVSCode/issues/353) +- Add syntax highlighting of code samples in hover defintion [#555](https://github.com/DonJayamanne/pythonVSCode/issues/555) +- Launch REPL for currently selected interpreter [#560](https://github.com/DonJayamanne/pythonVSCode/issues/560) +- Fixes to debugging of modules [#589](https://github.com/DonJayamanne/pythonVSCode/issues/589) +- Reminder to install jedi and ctags in Quick Start [#642](https://github.com/DonJayamanne/pythonVSCode/pull/642) +- Improvements to Symbol Provider [#622](https://github.com/DonJayamanne/pythonVSCode/pull/622) +- Changes to disable unit test prompts for workspace [#559](https://github.com/DonJayamanne/pythonVSCode/issues/559) +- Minor fixes [#627](https://github.com/DonJayamanne/pythonVSCode/pull/627) ## Version 0.5.5 (25 November 2016) -* Fixes to debugging of unittests (nose and pytest) [#543](https://github.com/DonJayamanne/pythonVSCode/issues/543) -* Fixes to debugging of Django [#546](https://github.com/DonJayamanne/pythonVSCode/issues/546) + +- Fixes to debugging of unittests (nose and pytest) [#543](https://github.com/DonJayamanne/pythonVSCode/issues/543) +- Fixes to debugging of Django [#546](https://github.com/DonJayamanne/pythonVSCode/issues/546) ## Version 0.5.4 (24 November 2016) -* Fixes to installing missing packages [#544](https://github.com/DonJayamanne/pythonVSCode/issues/544) -* Fixes to indentation of blocks of code [#432](https://github.com/DonJayamanne/pythonVSCode/issues/432) -* Fixes to debugging of unittests [#543](https://github.com/DonJayamanne/pythonVSCode/issues/543) -* Fixes to extension when a workspace (folder) isn't open [#542](https://github.com/DonJayamanne/pythonVSCode/issues/542) + +- Fixes to installing missing packages [#544](https://github.com/DonJayamanne/pythonVSCode/issues/544) +- Fixes to indentation of blocks of code [#432](https://github.com/DonJayamanne/pythonVSCode/issues/432) +- Fixes to debugging of unittests [#543](https://github.com/DonJayamanne/pythonVSCode/issues/543) +- Fixes to extension when a workspace (folder) isn't open [#542](https://github.com/DonJayamanne/pythonVSCode/issues/542) ## Version 0.5.3 (23 November 2016) -* Added support for [PySpark](http://spark.apache.org/docs/0.9.0/python-programming-guide.html) [#539](https://github.com/DonJayamanne/pythonVSCode/pull/539), [#540](https://github.com/DonJayamanne/pythonVSCode/pull/540) -* Debugging unittests (UnitTest, pytest, nose) [#333](https://github.com/DonJayamanne/pythonVSCode/issues/333) -* Displaying progress for formatting [#327](https://github.com/DonJayamanne/pythonVSCode/issues/327) -* Auto indenting ```else:``` inside ```if``` and similar code blocks [#432](https://github.com/DonJayamanne/pythonVSCode/issues/432) -* Prefixing new lines with '#' when new lines are added in the middle of a comment string [#365](https://github.com/DonJayamanne/pythonVSCode/issues/365) -* Debugging python modules [#518](https://github.com/DonJayamanne/pythonVSCode/issues/518), [#354](https://github.com/DonJayamanne/pythonVSCode/issues/354) - + Use new debug configuration ```Python Module``` -* Added support for workspace symbols using Exuberant CTags [#138](https://github.com/DonJayamanne/pythonVSCode/issues/138) - + New command ```Python: Build Workspace Symbols``` -* Added ability for linter to ignore paths or files [#501](https://github.com/DonJayamanne/pythonVSCode/issues/501) - + Add the following setting in ```settings.json``` + +- Added support for [PySpark](http://spark.apache.org/docs/0.9.0/python-programming-guide.html) [#539](https://github.com/DonJayamanne/pythonVSCode/pull/539), [#540](https://github.com/DonJayamanne/pythonVSCode/pull/540) +- Debugging unittests (UnitTest, pytest, nose) [#333](https://github.com/DonJayamanne/pythonVSCode/issues/333) +- Displaying progress for formatting [#327](https://github.com/DonJayamanne/pythonVSCode/issues/327) +- Auto indenting `else:` inside `if` and similar code blocks [#432](https://github.com/DonJayamanne/pythonVSCode/issues/432) +- Prefixing new lines with '#' when new lines are added in the middle of a comment string [#365](https://github.com/DonJayamanne/pythonVSCode/issues/365) +- Debugging python modules [#518](https://github.com/DonJayamanne/pythonVSCode/issues/518), [#354](https://github.com/DonJayamanne/pythonVSCode/issues/354) + - Use new debug configuration `Python Module` +- Added support for workspace symbols using Exuberant CTags [#138](https://github.com/DonJayamanne/pythonVSCode/issues/138) + - New command `Python: Build Workspace Symbols` +- Added ability for linter to ignore paths or files [#501](https://github.com/DonJayamanne/pythonVSCode/issues/501) + - Add the following setting in `settings.json` + ```python "python.linting.ignorePatterns": [ ".vscode/*.py", "**/site-packages/**/*.py" ], ``` -* Automatically adding brackets when autocompleting functions/methods [#425](https://github.com/DonJayamanne/pythonVSCode/issues/425) - + To enable this feature, turn on the setting ```"python.autoComplete.addBrackets": true``` -* Running nose tests with the arguments '--with-xunit' and '--xunit-file' [#517](https://github.com/DonJayamanne/pythonVSCode/issues/517) -* Added support for workspaceRootFolderName in settings.json [#525](https://github.com/DonJayamanne/pythonVSCode/pull/525), [#522](https://github.com/DonJayamanne/pythonVSCode/issues/522) -* Added support for workspaceRootFolderName in settings.json [#525](https://github.com/DonJayamanne/pythonVSCode/pull/525), [#522](https://github.com/DonJayamanne/pythonVSCode/issues/522) -* Fixes to running code in terminal [#515](https://github.com/DonJayamanne/pythonVSCode/issues/515) + +- Automatically adding brackets when autocompleting functions/methods [#425](https://github.com/DonJayamanne/pythonVSCode/issues/425) + - To enable this feature, turn on the setting `"python.autoComplete.addBrackets": true` +- Running nose tests with the arguments '--with-xunit' and '--xunit-file' [#517](https://github.com/DonJayamanne/pythonVSCode/issues/517) +- Added support for workspaceRootFolderName in settings.json [#525](https://github.com/DonJayamanne/pythonVSCode/pull/525), [#522](https://github.com/DonJayamanne/pythonVSCode/issues/522) +- Added support for workspaceRootFolderName in settings.json [#525](https://github.com/DonJayamanne/pythonVSCode/pull/525), [#522](https://github.com/DonJayamanne/pythonVSCode/issues/522) +- Fixes to running code in terminal [#515](https://github.com/DonJayamanne/pythonVSCode/issues/515) ## Version 0.5.2 -* Fix issue with mypy linter [#505](https://github.com/DonJayamanne/pythonVSCode/issues/505) -* Fix auto completion for files with different encodings [#496](https://github.com/DonJayamanne/pythonVSCode/issues/496) -* Disable warnings when debugging Django version prior to 1.8 [#479](https://github.com/DonJayamanne/pythonVSCode/issues/479) -* Prompt to save changes when refactoring without saving any changes [#441](https://github.com/DonJayamanne/pythonVSCode/issues/441) -* Prompt to save changes when renaminv without saving any changes [#443](https://github.com/DonJayamanne/pythonVSCode/issues/443) -* Use editor indentation size when refactoring code [#442](https://github.com/DonJayamanne/pythonVSCode/issues/442) -* Add support for custom jedi paths [#500](https://github.com/DonJayamanne/pythonVSCode/issues/500) + +- Fix issue with mypy linter [#505](https://github.com/DonJayamanne/pythonVSCode/issues/505) +- Fix auto completion for files with different encodings [#496](https://github.com/DonJayamanne/pythonVSCode/issues/496) +- Disable warnings when debugging Django version prior to 1.8 [#479](https://github.com/DonJayamanne/pythonVSCode/issues/479) +- Prompt to save changes when refactoring without saving any changes [#441](https://github.com/DonJayamanne/pythonVSCode/issues/441) +- Prompt to save changes when renaminv without saving any changes [#443](https://github.com/DonJayamanne/pythonVSCode/issues/443) +- Use editor indentation size when refactoring code [#442](https://github.com/DonJayamanne/pythonVSCode/issues/442) +- Add support for custom jedi paths [#500](https://github.com/DonJayamanne/pythonVSCode/issues/500) ## Version 0.5.1 -* Prompt to install linter if not installed [#255](https://github.com/DonJayamanne/pythonVSCode/issues/255) -* Prompt to configure and install test framework -* Added support for pylama [#495](https://github.com/DonJayamanne/pythonVSCode/pull/495) -* Partial support for PEP484 -* Linting python files when they are opened [#462](https://github.com/DonJayamanne/pythonVSCode/issues/462) -* Fixes to unit tests discovery [#307](https://github.com/DonJayamanne/pythonVSCode/issues/307), -[#459](https://github.com/DonJayamanne/pythonVSCode/issues/459) -* Fixes to intelliense [#438](https://github.com/DonJayamanne/pythonVSCode/issues/438), -[#433](https://github.com/DonJayamanne/pythonVSCode/issues/433), -[#457](https://github.com/DonJayamanne/pythonVSCode/issues/457), -[#436](https://github.com/DonJayamanne/pythonVSCode/issues/436), -[#434](https://github.com/DonJayamanne/pythonVSCode/issues/434), -[#447](https://github.com/DonJayamanne/pythonVSCode/issues/447), -[#448](https://github.com/DonJayamanne/pythonVSCode/issues/448), -[#293](https://github.com/DonJayamanne/pythonVSCode/issues/293), -[#381](https://github.com/DonJayamanne/pythonVSCode/pull/381) -* Supporting additional search paths for interpreters on windows [#446](https://github.com/DonJayamanne/pythonVSCode/issues/446) -* Fixes to code refactoring [#440](https://github.com/DonJayamanne/pythonVSCode/issues/440), -[#467](https://github.com/DonJayamanne/pythonVSCode/issues/467), -[#468](https://github.com/DonJayamanne/pythonVSCode/issues/468), -[#445](https://github.com/DonJayamanne/pythonVSCode/issues/445) -* Fixes to linters [#463](https://github.com/DonJayamanne/pythonVSCode/issues/463) -[#439](https://github.com/DonJayamanne/pythonVSCode/issues/439), -* Bug fix in handling nosetest arguments [#407](https://github.com/DonJayamanne/pythonVSCode/issues/407) -* Better error handling when linter fails [#402](https://github.com/DonJayamanne/pythonVSCode/issues/402) -* Restoring extension specific formatting [#421](https://github.com/DonJayamanne/pythonVSCode/issues/421) -* Fixes to debugger (unwanted breakpoints) [#392](https://github.com/DonJayamanne/pythonVSCode/issues/392), [#379](https://github.com/DonJayamanne/pythonVSCode/issues/379) -* Support spaces in python path when executing in terminal [#428](https://github.com/DonJayamanne/pythonVSCode/pull/428) -* Changes to snippets [#429](https://github.com/DonJayamanne/pythonVSCode/pull/429) -* Marketplace changes [#430](https://github.com/DonJayamanne/pythonVSCode/pull/430) -* Cleanup and miscellaneous fixes (typos, keyboard bindings and the liks) + +- Prompt to install linter if not installed [#255](https://github.com/DonJayamanne/pythonVSCode/issues/255) +- Prompt to configure and install test framework +- Added support for pylama [#495](https://github.com/DonJayamanne/pythonVSCode/pull/495) +- Partial support for PEP484 +- Linting python files when they are opened [#462](https://github.com/DonJayamanne/pythonVSCode/issues/462) +- Fixes to unit tests discovery [#307](https://github.com/DonJayamanne/pythonVSCode/issues/307), + [#459](https://github.com/DonJayamanne/pythonVSCode/issues/459) +- Fixes to intellisense [#438](https://github.com/DonJayamanne/pythonVSCode/issues/438), + [#433](https://github.com/DonJayamanne/pythonVSCode/issues/433), + [#457](https://github.com/DonJayamanne/pythonVSCode/issues/457), + [#436](https://github.com/DonJayamanne/pythonVSCode/issues/436), + [#434](https://github.com/DonJayamanne/pythonVSCode/issues/434), + [#447](https://github.com/DonJayamanne/pythonVSCode/issues/447), + [#448](https://github.com/DonJayamanne/pythonVSCode/issues/448), + [#293](https://github.com/DonJayamanne/pythonVSCode/issues/293), + [#381](https://github.com/DonJayamanne/pythonVSCode/pull/381) +- Supporting additional search paths for interpreters on windows [#446](https://github.com/DonJayamanne/pythonVSCode/issues/446) +- Fixes to code refactoring [#440](https://github.com/DonJayamanne/pythonVSCode/issues/440), + [#467](https://github.com/DonJayamanne/pythonVSCode/issues/467), + [#468](https://github.com/DonJayamanne/pythonVSCode/issues/468), + [#445](https://github.com/DonJayamanne/pythonVSCode/issues/445) +- Fixes to linters [#463](https://github.com/DonJayamanne/pythonVSCode/issues/463) + [#439](https://github.com/DonJayamanne/pythonVSCode/issues/439), +- Bug fix in handling nosetest arguments [#407](https://github.com/DonJayamanne/pythonVSCode/issues/407) +- Better error handling when linter fails [#402](https://github.com/DonJayamanne/pythonVSCode/issues/402) +- Restoring extension specific formatting [#421](https://github.com/DonJayamanne/pythonVSCode/issues/421) +- Fixes to debugger (unwanted breakpoints) [#392](https://github.com/DonJayamanne/pythonVSCode/issues/392), [#379](https://github.com/DonJayamanne/pythonVSCode/issues/379) +- Support spaces in python path when executing in terminal [#428](https://github.com/DonJayamanne/pythonVSCode/pull/428) +- Changes to snippets [#429](https://github.com/DonJayamanne/pythonVSCode/pull/429) +- Marketplace changes [#430](https://github.com/DonJayamanne/pythonVSCode/pull/430) +- Cleanup and miscellaneous fixes (typos, keyboard bindings and the liks) ## Version 0.5.0 -* Remove dependency on zmq when using Jupyter or IPython (pure python solution) -* Added a default keybinding for ```Jupyter:Run Selection/Line``` of ```ctrl+alt+enter``` -* Changes to update settings.json with path to python using [native API](https://github.com/DonJayamanne/pythonVSCode/commit/bce22a2b4af87eaf40669c6360eff3675280cdad) -* Changes to use [native API](https://github.com/DonJayamanne/pythonVSCode/commit/bce22a2b4af87eaf40669c6360eff3675280cdad) for formatting when saving documents -* Reusing existing terminal instead of creating new terminals -* Limiting linter messages to opened documents (hide messages if document is closed) [#375](https://github.com/DonJayamanne/pythonVSCode/issues/375) -* Resolving extension load errors when [#375](https://github.com/DonJayamanne/pythonVSCode/issues/375) -* Fixes to discovering unittests [#386](https://github.com/DonJayamanne/pythonVSCode/issues/386) -* Fixes to sending code to terminal on Windows [#387](https://github.com/DonJayamanne/pythonVSCode/issues/387) -* Fixes to executing python file in terminal on Windows [#385](https://github.com/DonJayamanne/pythonVSCode/issues/385) -* Fixes to launching local help (documentation) on Linux -* Fixes to typo in configuration documentation [#391](https://github.com/DonJayamanne/pythonVSCode/pull/391) -* Fixes to use ```python.pythonPath``` when sorting imports [#393](https://github.com/DonJayamanne/pythonVSCode/pull/393) -* Fixes to linters to handle situations when line numbers aren't returned [#399](https://github.com/DonJayamanne/pythonVSCode/pull/399) -* Fixes to signature tooltips when docstring is very long [#368](https://github.com/DonJayamanne/pythonVSCode/issues/368), [#113](https://github.com/DonJayamanne/pythonVSCode/issues/113) + +- Remove dependency on zmq when using Jupyter or IPython (pure python solution) +- Added a default keybinding for `Jupyter:Run Selection/Line` of `ctrl+alt+enter` +- Changes to update settings.json with path to python using [native API](https://github.com/DonJayamanne/pythonVSCode/commit/bce22a2b4af87eaf40669c6360eff3675280cdad) +- Changes to use [native API](https://github.com/DonJayamanne/pythonVSCode/commit/bce22a2b4af87eaf40669c6360eff3675280cdad) for formatting when saving documents +- Reusing existing terminal instead of creating new terminals +- Limiting linter messages to opened documents (hide messages if document is closed) [#375](https://github.com/DonJayamanne/pythonVSCode/issues/375) +- Resolving extension load errors when [#375](https://github.com/DonJayamanne/pythonVSCode/issues/375) +- Fixes to discovering unittests [#386](https://github.com/DonJayamanne/pythonVSCode/issues/386) +- Fixes to sending code to terminal on Windows [#387](https://github.com/DonJayamanne/pythonVSCode/issues/387) +- Fixes to executing python file in terminal on Windows [#385](https://github.com/DonJayamanne/pythonVSCode/issues/385) +- Fixes to launching local help (documentation) on Linux +- Fixes to typo in configuration documentation [#391](https://github.com/DonJayamanne/pythonVSCode/pull/391) +- Fixes to use `python.pythonPath` when sorting imports [#393](https://github.com/DonJayamanne/pythonVSCode/pull/393) +- Fixes to linters to handle situations when line numbers aren't returned [#399](https://github.com/DonJayamanne/pythonVSCode/pull/399) +- Fixes to signature tooltips when docstring is very long [#368](https://github.com/DonJayamanne/pythonVSCode/issues/368), [#113](https://github.com/DonJayamanne/pythonVSCode/issues/113) ## Version 0.4.2 -* Fix for autocompletion and code navigation with unicode characters [#372](https://github.com/DonJayamanne/pythonVSCode/issues/372), [#364](https://github.com/DonJayamanne/pythonVSCode/issues/364) + +- Fix for autocompletion and code navigation with unicode characters [#372](https://github.com/DonJayamanne/pythonVSCode/issues/372), [#364](https://github.com/DonJayamanne/pythonVSCode/issues/364) ## Version 0.4.1 -* Debugging of [Django templates](https://github.com/DonJayamanne/pythonVSCode/wiki/Debugging-Django#templates) -* Linting with [mypy](https://github.com/DonJayamanne/pythonVSCode/wiki/Linting#mypy) -* Improved error handling when loading [Jupyter/IPython](https://github.com/DonJayamanne/pythonVSCode/wiki/Jupyter-(IPython)) -* Fixes to unittests + +- Debugging of [Django templates](https://github.com/DonJayamanne/pythonVSCode/wiki/Debugging-Django#templates) +- Linting with [mypy](https://github.com/DonJayamanne/pythonVSCode/wiki/Linting#mypy) +- Improved error handling when loading [Jupyter/IPython]() +- Fixes to unittests ## Version 0.4.0 -* Added support for [Jupyter/IPython](https://github.com/DonJayamanne/pythonVSCode/wiki/Jupyter-(IPython)) -* Added local help (offline documentation) -* Added ability to pass in extra arguments to interpreter when executing scripts ([#316](https://github.com/DonJayamanne/pythonVSCode/issues/316)) -* Added ability set current working directory as the script file directory, when to executing a Python script -* Rendering intellisense icons correctly ([#322](https://github.com/DonJayamanne/pythonVSCode/issues/322)) -* Changes to capitalization of context menu text ([#320](https://github.com/DonJayamanne/pythonVSCode/issues/320)) -* Bug fix to running pydocstyle linter on windows ([#317](https://github.com/DonJayamanne/pythonVSCode/issues/317)) -* Fixed performance issues with regards to code navigation, displaying code Symbols and the like ([#324](https://github.com/DonJayamanne/pythonVSCode/issues/324)) -* Fixed code renaming issue when renaming imports ([#325](https://github.com/DonJayamanne/pythonVSCode/issues/325)) -* Fixed issue with the execution of the command ```python.execInTerminal``` via a shortcut ([#340](https://github.com/DonJayamanne/pythonVSCode/issues/340)) -* Fixed issue with code refactoring ([#363](https://github.com/DonJayamanne/pythonVSCode/issues/363)) + +- Added support for [Jupyter/IPython]() +- Added local help (offline documentation) +- Added ability to pass in extra arguments to interpreter when executing scripts ([#316](https://github.com/DonJayamanne/pythonVSCode/issues/316)) +- Added ability set current working directory as the script file directory, when to executing a Python script +- Rendering intellisense icons correctly ([#322](https://github.com/DonJayamanne/pythonVSCode/issues/322)) +- Changes to capitalization of context menu text ([#320](https://github.com/DonJayamanne/pythonVSCode/issues/320)) +- Bug fix to running pydocstyle linter on windows ([#317](https://github.com/DonJayamanne/pythonVSCode/issues/317)) +- Fixed performance issues with regards to code navigation, displaying code Symbols and the like ([#324](https://github.com/DonJayamanne/pythonVSCode/issues/324)) +- Fixed code renaming issue when renaming imports ([#325](https://github.com/DonJayamanne/pythonVSCode/issues/325)) +- Fixed issue with the execution of the command `python.execInTerminal` via a shortcut ([#340](https://github.com/DonJayamanne/pythonVSCode/issues/340)) +- Fixed issue with code refactoring ([#363](https://github.com/DonJayamanne/pythonVSCode/issues/363)) ## Version 0.3.24 -* Added support for clearing cached tests [#307](https://github.com/DonJayamanne/pythonVSCode/issues/307) -* Added support for executing files in terminal with spaces in paths [#308](https://github.com/DonJayamanne/pythonVSCode/issues/308) -* Fix issue related to running unittests on Windows [#309](https://github.com/DonJayamanne/pythonVSCode/issues/309) -* Support custom environment variables when launching external terminal [#311](https://github.com/DonJayamanne/pythonVSCode/issues/311) + +- Added support for clearing cached tests [#307](https://github.com/DonJayamanne/pythonVSCode/issues/307) +- Added support for executing files in terminal with spaces in paths [#308](https://github.com/DonJayamanne/pythonVSCode/issues/308) +- Fix issue related to running unittests on Windows [#309](https://github.com/DonJayamanne/pythonVSCode/issues/309) +- Support custom environment variables when launching external terminal [#311](https://github.com/DonJayamanne/pythonVSCode/issues/311) ## Version 0.3.23 -* Added support for the attribute supportsRunInTerminal attribute in debugger [#304](https://github.com/DonJayamanne/pythonVSCode/issues/304) -* Changes to ensure remote debugging resolves remote paths correctly [#302](https://github.com/DonJayamanne/pythonVSCode/issues/302) -* Added support for custom pytest and nosetest paths [#301](https://github.com/DonJayamanne/pythonVSCode/issues/301) -* Resolved issue in ```Watch``` window displaying ```= 8.9.1, < 9.0.0) -1. Python 2.7 or later (required only for testing the extension and running unit tests) -1. Windows, macOS, or Linux -1. Visual Studio Code -1. Following VS Code extensions: - * [TSLint](https://marketplace.visualstudio.com/items?itemName=eg2.tslint) - * [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) -1. Have an issue which has been accepted with a "needs PR" label (feel free to indicate you would be happy to provide a PR for the issue) - -### Setup - -```shell -git clone https://github.com/microsoft/vscode-python -cd vscode-python -npm install -python3 -m venv .venv -# Activate the virtual environment as appropriate for your shell. -python3 -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt -# Specifying the virtual environment simply varies between shells. -export CI_PYTHON_PATH=`pwd`/.venv/bin/python -``` - -You may 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 [Command Palette](https://code.visualstudio.com/docs/editor/tasks) (short cut `CTRL+SHIFT+B` or `⇧⌘B`) - -You can also compile from the command-line: - -```shell -tsc -p ./ # full compile -tsc --watch -p ./ # incremental -``` - -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: - -### Validate your changes - -To test the changes you launch a development version of VS Code on the workspace vscode, which you are currently editing. -Use the `Launch Extension` launch option. - -### Debugging Unit Tests - -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 `Debug Unit Tests` launch options. - -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=[The suite name of your unit test file]" - ], -``` -...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). - -### Debugging System Tests - -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 `Launch Test` and `Launch Multiroot Tests` launch options. -1. **Note** you will be running tests under the default Python interpreter for the system. - -*Change the version of python the tests are executed with by setting the `CI_PYTHON_PATH`.* - -Tests will be executed using the system default interpreter (whatever that is for your local machine), unless you explicitly set the `CI_PYTHON_PATH` environment variable. To test against different versions of Python you *must* use this. - -In the launch.json file, you can add the following to the `Launch Tests` setting to easily change the interpreter used during testing: - -```js - "env":{ - "CI_PYTHON_PATH": "/path/to/interpreter/of/choice/python" - } -``` - -You can also run them from the command-line (after compiling): - -```shell -npm run testSingleWorkspace # will launch the VSC UI -npm run testMultiWorkspace # will launch the VSC UI -``` -...note this will use the Python interpreter that your current shell is making use of, no need to set `CI_PYTHON_PATH` here. - -*To limit system tests to a specific suite* - -If you are running system tests (we call them *system* tests, others call them *integration* or otherwise) and you wish to run a specific test suite, edit the `src/test/index.ts` file here: - -https://github.com/Microsoft/vscode-python/blob/b328ba12331ed34a267e32e77e3e4b1eff235c13/src/test/index.ts#L21 - -...and identify the test suite you want to run/debug like this: - -```ts -const grep = '[The suite name of your *test.ts file]'; // IS_CI_SERVER &&... -``` -...and then use the `Launch Tests` debugger launcher. This will run only the suite you name in the grep. - -And be sure to escape any grep-sensitive characters in your suite name (and to remove the change from src/test/index.ts before you submit). - -### Standard Debugging - -Clone the repo into any directory, open that directory in VSCode, and use the `Launch 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 ve 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 two week sprints. We start a sprint every other Wednesday. -You can look at the newest -[milestone](https://github.com/Microsoft/vscode-python/milestones) to see when -the current sprint ends. 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 in a sprint, but if they are not completed they will not -block a release. All other issues are considered best-effort for that -sprint. - -The extension aims to do a new release every four weeks (two sprints). 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. It is what 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/tree/release). -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` -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. Typically, on new issues, the `needs` label is either `needs verification` or `needs more info`. 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 [`needs verification`](https://github.com/Microsoft/vscode-python/labels/needs%20verification). Issues with this label have not been verified to be an actual problem (e.g. making sure the reported issue is not caused by the user's configuration or machine). - -The other way to help is to go through issues that are labeled as [`validate fix`](https://github.com/Microsoft/vscode-python/labels/validate%20fix). These issues are believed to be fixed, but having an independent validation is always appreciated. - -#### Closed issues - -If a closed issue is labeled with ["volunteer"](https://github.com/Microsoft/vscode-python/issues?q=label%3Avolunteer+is%3Aclosed) that means the development team has no plans to implement the work for the issue, but that we would accept a pull request if provided. We close these types of issues to help us stay focused on the issues we do plan/hope to get to (which we will also accept pull requests for, just please discuss design considerations with us first to help make sure your pull request will be accepted). - -### 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. As such, the major version -is the current year, the minor version is the month when feature -freeze was reached, and the micro version is how many releases there -have been in that month (starting at 0). For example -the release made when we reach feature freeze in July 2018 -would be `2018.7.0`, and if there is a second release in that month -it would be `2018.7.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)). - -## Development Build - -We publish the latest development -build of the extension onto a cloud storage provider. -If you are interested in helping us test our development builds or would like -to stay ahead of the curve, then please feel free to download and install the -extension 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. - -The development build of the extension: - -* Will be replaced with new releases published onto the - [VS Code Marketplace](https://marketplace.visualstudio.com/VSCode). -* Does not get updated with new development builds of the extension (if you want to - test a newer development build, uninstall the old version of the - extension and then install the new version) -* Is built everytime a PR is commited into the [`master` branch](https://github.com/Microsoft/vscode-python). - -### Installing the extension from a git clone - -If you would like to have a copy of the extension installed from a git clone so it can be refreshed regularly, the [`pvsc-dev-ext.py` script](https://github.com/Microsoft/vscode-python/blob/master/pvsc-dev-ext.py) will help facilitate that. The script provides two commands. - -To create the git clone and do the initial build, use the `setup` command: -``` -$ python3 pvsc-dev-ext.py setup stable -``` -You may choose to have the script set up either a stable or insiders install of VS Code. - -Once the extension is set up with a dev install, you can update it at any time to match what is in the `master` branch by using the `update` command: -``` -$ python3 pvsc-dev-ext.py update -``` -This will update stable and/or insiders installs of the extension. You can run this command at e.g. startup of your computer to make sure you are always using the latest version of the extension in VS Code. +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 8fa5e4f75f19..000000000000 --- a/PYTHON_INTERACTIVE_TROUBLESHOOTING.md +++ /dev/null @@ -1,71 +0,0 @@ -# Trouble shooting the Python Interactive Window - -This document is intended to help troubleshoot problems in the Python Interactive Window. - ---- -## Jupyter Not Installed -This error can happen when you - -* Don't have Jupyter installed -* Have picked the wrong Python environment (one that doesn't have Jupyter installed). - -### The first step is to verify you are running the Python environment you want. - -The python you're using is picked with the selection dropdown on the bottom left of the VS Code window: - -![selector](resources/PythonSelector.png) - -To verify this version of python supports Jupyter notebooks, start a 'Python: REPL' from the command palette -and then type in the following code: - -```python -import jupyter_core -import notebook -jupyter_core.version_info -notebook.version_info -``` -If any of these commands fail, the python you have selected doesn't support launching jupyter notebooks. - -Failures would look something like: - -``` ->>> import jupyter -Traceback (most recent call last): - File "", line 1, in -ImportError: No module named jupyter ->>> import notebook -Traceback (most recent call last): - File "", line 1, in -ImportError: No module named notebook ->>> -``` - -### The second step (if changing the Python version doesn't work) is 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 40aaf6e46250..e9dd52a538cd 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,119 @@ # 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.4), including features such as linting, debugging, IntelliSense, code navigation, code formatting, refactoring, unit tests, 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 3.** Open or create a Python file and start coding! +- **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 + + + +- Select your Python interpreter by clicking on the status bar -## Optional steps -* **Step 4.** [Install a linter](https://code.visualstudio.com/docs/python/linting) to get errors and warnings -- you can further customize linting rules to fit your needs. -* **Step 5.** Select your preferred Python interpreter/version/environment using the `Select Interpreter` command. - + By default we use the one that's on your path. - + If you have a workspace open you can also click in the status bar to change the interpreter. -* **Step 6.** Install `ctags` for Workspace Symbols, from [here](http://ctags.sourceforge.net/), or using `brew install ctags` on macOS. + + +- 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: -* [Follow our Python tutorial](https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites) with step-by-step instructions for building a simple app. -* Check out the [Python documentation on the VS Code site](https://code.visualstudio.com/docs/languages/python) for general information about using the extension. + +- [Follow our Python tutorial](https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites) with step-by-step instructions for building a simple app. +- Check out the [Python documentation on the VS Code site](https://code.visualstudio.com/docs/languages/python) for general information about using the extension. +- Check out the [Jupyter Notebook documentation on the VS Code site](https://code.visualstudio.com/docs/python/jupyter-support) for information about using Jupyter Notebooks in VS Code. ## Useful commands Open the Command Palette (Command+Shift+P on macOS and Ctrl+Shift+P on Windows/Linux) and type in one of the following commands: -Command | Description ---- | --- -```Python: Select Interpreter``` | Switch between Python interpreters, versions, and environments. -```Python: Start REPL``` | Start an interactive Python REPL using the selected interpreter in the VS Code terminal. -```Python: Run Python File in Terminal``` | Runs the active Python file in the VS Code terminal. You can also run a Python file by right-clicking on the file and selecting ```Run Python File in Terminal```. -```Python: Select Linter``` | Switch from PyLint to flake8 or other supported linters. +| Command | Description | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Python: Select Interpreter` | Switch between Python interpreters, versions, and environments. | +| `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: 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`. -## Supported locales +## Feature details -The extension is available in multiple languages thanks to external -contributors (if you would like to contribute a translation, see the -[pull request which added Italian](https://github.com/Microsoft/vscode-python/pull/1152)): - -* `de` -* `en` -* `es` -* `fr` -* `it` -* `ja` -* `ko-kr` -* `pt-br` -* `ru` -* `zh-cn` -* `zh-tw` +Learn more about the rich features of the Python extension: -## Questions, issues, feature requests, and contributions +- [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). -* 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 -* 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) -## Feature details -* IDE-like features - + Automatic indenting - + Code navigation ("Go to", "Find all" references) - + Code definition (Peek and hover definition, View signatures) - + Rename refactoring - + Sorting import statements (use the `Python: Sort Imports` command) -* Intellisense and autocomplete (including PEP 484 and PEP 526 support) - + Ability to include custom module paths (e.g. include paths for libraries like Google App Engine, etc.; use the setting `python.autoComplete.extraPaths = []`) -* Code formatting - + Auto formatting of code upon saving changes (default to 'Off') - + Use either [yapf](https://pypi.org/project/yapf/), [autopep8](https://pypi.org/project/autopep8/), or [Black](https://pypi.org/project/black/) for code formatting (defaults to autopep8) -* Linting - + Support for multiple linters with custom settings (default is [Pylint](https://pypi.org/project/pylint/), but [Prospector](https://pypi.org/project/prospector/), [Flake8](https://pypi.org/project/flake8/), [pylama](https://pypi.org/project/pylama/), [pydocstyle](https://pypi.org/project/pydocstyle/), and [mypy](https://pypi.org/project/mypy/) are also supported) -* Debugging - + Watch window - + Evaluate expressions - + Step through code ("Step in", "Step out", "Continue") - + Add/remove breakpoints - + Local variables and arguments - + Multi-threaded applications - + Web applications (such as [Flask](http://flask.pocoo.org/) & [Django](https://www.djangoproject.com/), with template debugging) - + Expanding values (viewing children, properties, etc) - + Conditional breakpoints - + Remote debugging (over SSH) - + Google App Engine - + Debugging in the integrated or external terminal window - + Debugging as sudo -* Unit testing - + Support for [unittest](https://docs.python.org/3/library/unittest.html#module-unittest), [pytest](https://pypi.org/project/pytest/), and [nose](https://pypi.org/project/nose/) - + Ability to run all failed tests, individual tests - + Debugging unit tests -* Snippets -* Miscellaneous - + Running a file or selected text in python terminal - + Automatic activation of environments in the terminal -* Refactoring - + Rename refactorings - + Extract variable refactorings - + Extract method refactorings - + Sort imports - -![General Features](https://raw.githubusercontent.com/microsoft/vscode-python/master/images/general.gif) - -![Debugging](https://raw.githubusercontent.com/microsoft/vscode-python/master/images/debugDemo.gif) - -![Unit Tests](https://raw.githubusercontent.com/microsoft/vscode-python/master/images/unittest.gif) +## Supported locales + +The extension is available in multiple languages: `de`, `en`, `es`, `fa`, `fr`, `it`, `ja`, `ko-kr`, `nl`, `pl`, `pt-br`, `ru`, `tr`, `zh-cn`, `zh-tw` + +## Questions, issues, feature requests, and contributions +- If you have a question about how to accomplish something with the extension, please [ask on 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 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 @@ -116,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/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..1ceb287afafa --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability]() of a security vulnerability, please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + +- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) +- Full paths of source file(s) related to the manifestation of the issue +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + + diff --git a/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 a7df598626d7..000000000000 --- a/ThirdPartyNotices-Distribution.txt +++ /dev/null @@ -1,11736 +0,0 @@ -THIRD-PARTY SOFTWARE NOTICES AND INFORMATION -Do Not Translate or Localize - -Microsoft Python extension for Visual Studio Code incorporates third party material from the projects listed below. - - - -1. @babel/runtime-corejs2 7.1.2 (https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.1.2.tgz) -2. @emotion/hash 0.6.6 (https://registry.npmjs.org/@emotion/hash/-/hash-0.6.6.tgz) -3. @emotion/memoize 0.6.6 (https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.6.tgz) -4. @emotion/stylis 0.7.1 (https://registry.npmjs.org/@emotion/stylis/-/stylis-0.7.1.tgz) -5. @emotion/unitless 0.6.7 (https://registry.npmjs.org/@emotion/unitless/-/unitless-0.6.7.tgz) -6. @jupyterlab/coreutils 2.1.4 (https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-2.1.4.tgz) -7. @jupyterlab/observables 2.0.7 (https://registry.npmjs.org/@jupyterlab/observables/-/observables-2.0.7.tgz) -8. @jupyterlab/services 3.1.4 (https://registry.npmjs.org/@jupyterlab/services/-/services-3.1.4.tgz) -9. @mapbox/polylabel 1.0.2 (https://registry.npmjs.org/@mapbox/polylabel/-/polylabel-1.0.2.tgz) -10. @nteract/markdown 2.1.4 (https://registry.npmjs.org/@nteract/markdown/-/markdown-2.1.4.tgz) -11. @nteract/mathjax 2.1.4 (https://registry.npmjs.org/@nteract/mathjax/-/mathjax-2.1.4.tgz) -12. @nteract/octicons 0.4.3 (https://registry.npmjs.org/@nteract/octicons/-/octicons-0.4.3.tgz) -13. @nteract/plotly 1.0.0 (https://registry.npmjs.org/@nteract/plotly/-/plotly-1.0.0.tgz) -14. @nteract/transform-dataresource 4.3.5 (https://registry.npmjs.org/@nteract/transform-dataresource/-/transform-dataresource-4.3.5.tgz) -15. @nteract/transform-geojson 3.2.3 (https://registry.npmjs.org/@nteract/transform-geojson/-/transform-geojson-3.2.3.tgz) -16. @nteract/transform-model-debug 3.2.3 (https://registry.npmjs.org/@nteract/transform-model-debug/-/transform-model-debug-3.2.3.tgz) -17. @nteract/transform-plotly 3.2.3 (https://registry.npmjs.org/@nteract/transform-plotly/-/transform-plotly-3.2.3.tgz) -18. @nteract/transform-vdom 2.2.3 (https://registry.npmjs.org/@nteract/transform-vdom/-/transform-vdom-2.2.3.tgz) -19. @nteract/transforms 4.4.4 (https://registry.npmjs.org/@nteract/transforms/-/transforms-4.4.4.tgz) -20. @phosphor/algorithm 1.1.2 (https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.1.2.tgz) -21. @phosphor/collections 1.1.2 (https://registry.npmjs.org/@phosphor/collections/-/collections-1.1.2.tgz) -22. @phosphor/coreutils 1.3.0 (https://registry.npmjs.org/@phosphor/coreutils/-/coreutils-1.3.0.tgz) -23. @phosphor/disposable 1.1.2 (https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.1.2.tgz) -24. @phosphor/messaging 1.2.2 (https://registry.npmjs.org/@phosphor/messaging/-/messaging-1.2.2.tgz) -25. @phosphor/signaling 1.2.2 (https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.2.2.tgz) -26. _pydev_calltip_util.py (for PyDev.Debugger) (https://github.com/fabioz/PyDev.Debugger/blob/master/_pydev_bundle/_pydev_calltip_util.py) -27. ajv 5.5.2 (https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz) -28. angular.io (for RxJS 5.5) (https://angular.io/) -29. anser 1.4.7 (https://registry.npmjs.org/anser/-/anser-1.4.7.tgz) -30. ansi-to-html 0.6.7 (https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.6.7.tgz) -31. ansi-to-react 3.3.3 (https://registry.npmjs.org/ansi-to-react/-/ansi-to-react-3.3.3.tgz) -32. applicationinsights 1.0.6 (https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.0.6.tgz) -33. arch 2.1.0 (https://registry.npmjs.org/arch/-/arch-2.1.0.tgz) -34. arr-diff 4.0.0 (https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz) -35. arr-flatten 1.1.0 (https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz) -36. arr-union 3.1.0 (https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz) -37. array-unique 0.3.2 (https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz) -38. asn1 0.2.3 (https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz) -39. assert-plus 1.0.0 (https://github.com/joyent/node-assert-plus/tree/v1.0.0) -40. assign-symbols 1.0.0 (https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz) -41. asynckit 0.4.0 (https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz) -42. atob 2.1.1 (https://registry.npmjs.org/atob/-/atob-2.1.1.tgz) -43. aws-sign2 0.7.0 (https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz) -44. aws4 1.7.0 (https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz) -45. azure-storage 2.10.1 (https://registry.npmjs.org/azure-storage/-/azure-storage-2.10.1.tgz) -46. babel-polyfill 6.26.0 (https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz) -47. babel-runtime 6.26.0 (https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz) -48. bail 1.0.3 (https://registry.npmjs.org/bail/-/bail-1.0.3.tgz) -49. balanced-match 1.0.0 (https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz) -50. base 0.11.2 (https://registry.npmjs.org/base/-/base-0.11.2.tgz) -51. base16 1.0.0 (https://registry.npmjs.org/base16/-/base16-1.0.0.tgz) -52. bcrypt-pbkdf 1.0.1 (https://www.npmjs.com/package/bcrypt-pbkdf) -53. bintrees 1.0.2 (https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz) -54. brace-expansion 1.1.11 (https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz) -55. braces 2.3.2 (https://registry.npmjs.org/braces/-/braces-2.3.2.tgz) -56. browserify-mime 1.2.9 (https://registry.npmjs.org/browserify-mime/-/browserify-mime-1.2.9.tgz) -57. cache-base 1.0.1 (https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz) -58. caseless 0.12.0 (https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz) -59. character-entities-legacy 1.1.2 (https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.2.tgz) -60. character-reference-invalid 1.1.2 (https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.2.tgz) -61. charenc 0.0.2 (https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz) -62. class-utils 0.3.6 (https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz) -63. classnames 2.2.6 (https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz) -64. co 4.6.0 (https://registry.npmjs.org/co/-/co-4.6.0.tgz) -65. collapse-white-space 1.0.4 (https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.4.tgz) -66. collection-visit 1.0.0 (https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz) -67. combined-stream 1.0.6 (https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz) -68. comment-json 1.1.3 (https://registry.npmjs.org/comment-json/-/comment-json-1.1.3.tgz) -69. component-emitter 1.2.1 (https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz) -70. concat-map 0.0.1 (https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz) -71. copy-descriptor 0.1.1 (https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz) -72. core-js 2.5.7 (https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz) -73. core-util-is 1.0.2 (https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz) -74. create-emotion 9.2.12 (https://registry.npmjs.org/create-emotion/-/create-emotion-9.2.12.tgz) -75. crypt 0.0.2 (https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz) -76. css-loader 1.0.1 (https://registry.npmjs.org/css-loader/-/css-loader-1.0.1.tgz) -77. d3-array 1.2.4 (https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz) -78. d3-bboxCollide 1.0.4 (https://registry.npmjs.org/d3-bboxCollide/-/d3-bboxCollide-1.0.4.tgz) -79. d3-brush 1.0.6 (https://registry.npmjs.org/d3-brush/-/d3-brush-1.0.6.tgz) -80. d3-chord 1.0.6 (https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz) -81. d3-collection 1.0.7 (https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz) -82. d3-color 1.2.3 (https://registry.npmjs.org/d3-color/-/d3-color-1.2.3.tgz) -83. d3-contour 1.3.2 (https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz) -84. d3-dispatch 1.0.5 (https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz) -85. d3-drag 1.2.3 (https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.3.tgz) -86. d3-ease 1.0.5 (https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz) -87. d3-force 1.1.2 (https://registry.npmjs.org/d3-force/-/d3-force-1.1.2.tgz) -88. d3-format 1.3.2 (https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz) -89. d3-glyphedge 1.2.0 (https://registry.npmjs.org/d3-glyphedge/-/d3-glyphedge-1.2.0.tgz) -90. d3-hexbin 0.2.2 (https://registry.npmjs.org/d3-hexbin/-/d3-hexbin-0.2.2.tgz) -91. d3-hierarchy 1.1.8 (https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz) -92. d3-interpolate 1.3.2 (https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz) -93. d3-path 1.0.7 (https://registry.npmjs.org/d3-path/-/d3-path-1.0.7.tgz) -94. d3-polygon 1.0.5 (https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz) -95. d3-quadtree 1.0.1 (https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.1.tgz) -96. d3-sankey-circular 0.25.0 (https://registry.npmjs.org/d3-sankey-circular/-/d3-sankey-circular-0.25.0.tgz) -97. d3-scale 1.0.7 (https://registry.npmjs.org/d3-scale/-/d3-scale-1.0.7.tgz) -98. d3-selection 1.3.2 (https://registry.npmjs.org/d3-selection/-/d3-selection-1.3.2.tgz) -99. d3-shape 1.2.2 (https://registry.npmjs.org/d3-shape/-/d3-shape-1.2.2.tgz) -100. d3-time 1.0.10 (https://registry.npmjs.org/d3-time/-/d3-time-1.0.10.tgz) -101. d3-time-format 2.1.3 (https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz) -102. d3-timer 1.0.9 (https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz) -103. d3-transition 1.1.3 (https://registry.npmjs.org/d3-transition/-/d3-transition-1.1.3.tgz) -104. d3-voronoi 1.1.4 (https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz) -105. dashdash 1.14.1 (https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz) -106. debug 2.6.9 (https://registry.npmjs.org/debug/-/debug-2.6.9.tgz) -107. decode-uri-component 0.2.0 (https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz) -108. define-property 2.0.2 (https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz) -109. delayed-stream 1.0.0 (https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz) -110. diagnostic-channel 0.2.0 (https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz) -111. diagnostic-channel-publishers 0.2.1 (https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz) -112. diff-match-patch 1.0.0 (https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.0.tgz) -113. dotenv 5.0.1 (https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz) -114. ecc-jsbn 0.1.1 (https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz) -115. emotion 9.2.12 (https://registry.npmjs.org/emotion/-/emotion-9.2.12.tgz) -116. encoding 0.1.12 (https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz) -117. entities 1.1.1 (https://registry.npmjs.org/entities/-/entities-1.1.1.tgz) -118. escape-carriage 1.2.0 (https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.2.0.tgz) -119. esprima 2.7.3 (https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz) -120. expand-brackets 2.1.4 (https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz) -121. extend 3.0.1 (https://registry.npmjs.org/extend/-/extend-3.0.1.tgz) -122. extend-shallow 3.0.2 (https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz) -123. extglob 2.0.4 (https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz) -124. extsprintf 1.3.0 (https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz) -125. fast-deep-equal 1.1.0 (https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz) -126. fast-json-stable-stringify 2.0.0 (https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz) -127. fbjs 0.8.17 (https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz) -128. file-matcher 1.3.0 (https://registry.npmjs.org/file-matcher/-/file-matcher-1.3.0.tgz) -129. fill-range 4.0.0 (https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz) -130. flat 4.0.0 (https://registry.npmjs.org/flat/-/flat-4.0.0.tgz) -131. for-in 1.0.2 (https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz) -132. forever-agent 0.6.1 (https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz) -133. form-data 2.3.2 (https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz) -134. fragment-cache 0.2.1 (https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz) -135. fs-extra 4.0.3 (https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz) -136. fs.realpath 1.0.0 (https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz) -137. fuzzy 0.1.3 (https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz) -138. get-port 3.2.0 (https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz) -139. get-value 2.0.6 (https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz) -140. getpass 0.1.7 (https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz) -141. glob 7.1.2 (https://registry.npmjs.org/glob/-/glob-7.1.2.tgz) -142. graceful-fs 4.1.11 (https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz) -143. har-schema 2.0.0 (https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz) -144. har-validator 5.0.3 (https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz) -145. has-value 1.0.0 (https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz) -146. has-values 1.0.0 (https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz) -147. hash-base 3.0.4 (https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz) -148. http-signature 1.2.0 (https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz) -149. iconv-lite 0.4.21 (https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz) -150. inflight 1.0.6 (https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz) -151. inherits 2.0.3 (https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz) -152. inversify 4.11.1 (https://registry.npmjs.org/inversify/-/inversify-4.11.1.tgz) -153. IPython (for PyDev.Debugger) (https://ipython.org/) -154. is-accessor-descriptor 0.1.6 (https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz) -155. is-alphabetical 1.0.2 (https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.2.tgz) -156. is-alphanumerical 1.0.2 (https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.2.tgz) -157. is-buffer 1.1.6 (https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz) -158. is-data-descriptor 0.1.4 (https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz) -159. is-decimal 1.0.2 (https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.2.tgz) -160. is-descriptor 0.1.6 (https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz) -161. is-extendable 0.1.1 (https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz) -162. is-hexadecimal 1.0.2 (https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz) -163. is-number 3.0.0 (https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz) -164. is-odd 2.0.0 (https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz) -165. is-plain-obj 1.1.0 (https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz) -166. is-plain-object 2.0.4 (https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz) -167. is-stream 1.1.0 (https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz) -168. is-typedarray 1.0.0 (https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz) -169. is-whitespace-character 1.0.2 (https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.2.tgz) -170. is-windows 1.0.2 (https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz) -171. is-word-character 1.0.2 (https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.2.tgz) -172. is-wsl 1.1.0 (https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz) -173. isarray 1.0.0 (https://github.com/juliangruber/isarray/blob/v1.0.0) -174. isobject 3.0.1 (https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz) -175. isort 4.3.4 (https://github.com/timothycrosley/isort/tree/4.3.4) -176. isstream 0.1.2 (https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz) -177. Jedi 0.12.0 (https://github.com/davidhalter/jedi/tree/v0.12.0) -178. jsbn 0.1.1 (https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz) -179. json-edm-parser 0.1.2 (https://registry.npmjs.org/json-edm-parser/-/json-edm-parser-0.1.2.tgz) -180. json-parser 1.1.5 (https://registry.npmjs.org/json-parser/-/json-parser-1.1.5.tgz) -181. json-schema 0.2.3 (https://www.npmjs.com/package/json-schema) -182. json-schema-traverse 0.3.1 (https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz) -183. json-stable-stringify 1.0.1 (https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz) -184. json-stringify-safe 5.0.1 (https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz) -185. json2csv 3.11.5 (https://registry.npmjs.org/json2csv/-/json2csv-3.11.5.tgz) -186. jsonfile 4.0.0 (https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz) -187. jsonify 0.0.0 (https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz) -188. jsonparse 1.2.0 (https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz) -189. jsprim 1.4.1 (https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz) -190. kind-of 6.0.2 (https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz) -191. labella 1.1.4 (https://registry.npmjs.org/labella/-/labella-1.1.4.tgz) -192. leaflet 1.3.4 (https://registry.npmjs.org/leaflet/-/leaflet-1.3.4.tgz) -193. line-by-line 0.1.6 (https://registry.npmjs.org/line-by-line/-/line-by-line-0.1.6.tgz) -194. lodash 4.17.11 (https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz) -195. lodash.clonedeep 4.5.0 (https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz) -196. lodash.curry 4.1.1 (https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz) -197. lodash.flatten 4.4.0 (https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz) -198. lodash.flow 3.5.0 (https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz) -199. lodash.get 4.4.2 (https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz) -200. lodash.set 4.3.2 (https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz) -201. lodash.uniq 4.5.0 (https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz) -202. map-cache 0.2.2 (https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz) -203. map-visit 1.0.0 (https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz) -204. markdown-escapes 1.0.2 (https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.2.tgz) -205. martinez-polygon-clipping 0.1.5 (https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.1.5.tgz) -206. material-colors 1.2.6 (https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz) -207. md5 2.2.1 (https://registry.npmjs.org/md5/-/md5-2.2.1.tgz) -208. md5.js 1.3.4 (https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz) -209. mdast-add-list-metadata 1.0.1 (https://registry.npmjs.org/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz) -210. memoize-one 4.0.0 (https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.0.tgz) -211. micromatch 3.1.10 (https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz) -212. mime-db 1.33.0 (https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz) -213. mime-types 2.1.18 (https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz) -214. minimatch 3.0.4 (https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz) -215. minimist 1.2.0 (https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz) -216. mixin-deep 1.3.1 (https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz) -217. moment 2.21.0 (http://registry.npmjs.org/moment/-/moment-2.21.0.tgz) -218. ms 2.0.0 (https://registry.npmjs.org/ms/-/ms-2.0.0.tgz) -219. named-js-regexp 1.3.3 (https://registry.npmjs.org/named-js-regexp/-/named-js-regexp-1.3.3.tgz) -220. nanomatch 1.2.9 (https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz) -221. node-fetch 1.7.3 (https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz) -222. node-stream-zip 1.6.0 (https://github.com/antelle/node-stream-zip/tree/1.6.0) -223. numeral 2.0.6 (https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz) -224. oauth-sign 0.8.2 (https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz) -225. object-assign 4.1.1 (https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz) -226. object-copy 0.1.0 (https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz) -227. object-visit 1.0.1 (https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz) -228. object.pick 1.3.0 (https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz) -229. once 1.4.0 (https://registry.npmjs.org/once/-/once-1.4.0.tgz) -230. opn 5.3.0 (https://registry.npmjs.org/opn/-/opn-5.3.0.tgz) -231. options 0.0.6 (https://registry.npmjs.org/options/-/options-0.0.6.tgz) -232. os-browserify 0.3.0 (https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz) -233. os-tmpdir 1.0.2 (https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz) -234. parse-entities 1.2.0 (https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.0.tgz) -235. parso 0.2.1 (https://github.com/davidhalter/parso/tree/v0.2.1) -236. pascalcase 0.1.1 (https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz) -237. path-browserify 0.0.0 (https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz) -238. path-is-absolute 1.0.1 (https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz) -239. path-posix 1.0.0 (https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz) -240. performance-now 2.1.0 (https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz) -241. pidusage 1.2.0 (https://registry.npmjs.org/pidusage/-/pidusage-1.2.0.tgz) -242. polygon-offset 0.3.1 (https://registry.npmjs.org/polygon-offset/-/polygon-offset-0.3.1.tgz) -243. posix-character-classes 0.1.1 (https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz) -244. prismjs 1.15.0 (https://registry.npmjs.org/prismjs/-/prismjs-1.15.0.tgz) -245. process 0.11.10 (https://registry.npmjs.org/process/-/process-0.11.10.tgz) -246. process-nextick-args 1.0.7 (https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz) -247. prop-types 15.6.2 (https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz) -248. psl 1.1.29 (https://github.com/wrangr/psl/tree/v1.1.29) -249. ptvsd 4.2.0 (https://github.com/Microsoft/ptvsd/tree/v4.2.0) -250. punycode 1.4.1 (https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz) -251. pure-color 1.3.0 (https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz) -252. py2app (for PyDev.Debugger) (https://bitbucket.org/ronaldoussoren/py2app) -253. PyDev.Debugger (for ptvsd 4) (https://pypi.org/project/pydevd/) -254. qs 6.5.2 (https://registry.npmjs.org/qs/-/qs-6.5.2.tgz) -255. querystringify 2.0.0 (https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz) -256. react 16.5.2 (https://registry.npmjs.org/react/-/react-16.5.2.tgz) -257. react-annotation 1.3.1 (https://registry.npmjs.org/react-annotation/-/react-annotation-1.3.1.tgz) -258. react-base16-styling 0.5.3 (https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.5.3.tgz) -259. react-color 2.14.1 (https://registry.npmjs.org/react-color/-/react-color-2.14.1.tgz) -260. react-dom 16.5.2 (https://registry.npmjs.org/react-dom/-/react-dom-16.5.2.tgz) -261. react-hot-loader 4.3.11 (https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.3.11.tgz) -262. react-json-tree 0.11.0 (https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.11.0.tgz) -263. react-markdown 3.6.0 (https://registry.npmjs.org/react-markdown/-/react-markdown-3.6.0.tgz) -264. react-table 6.8.6 (https://registry.npmjs.org/react-table/-/react-table-6.8.6.tgz) -265. react-table-hoc-fixed-columns 1.0.1 (https://registry.npmjs.org/react-table-hoc-fixed-columns/-/react-table-hoc-fixed-columns-1.0.1.tgz) -266. reactcss 1.2.3 (https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz) -267. readable-stream 2.0.6 (https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz) -268. reflect-metadata 0.1.12 (https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz) -269. regex-not 1.0.2 (https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz) -270. remark-parse 5.0.0 (https://registry.npmjs.org/remark-parse/-/remark-parse-5.0.0.tgz) -271. repeat-element 1.1.2 (https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz) -272. repeat-string 1.6.1 (https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz) -273. request 2.87.0 (https://registry.npmjs.org/request/-/request-2.87.0.tgz) -274. request-progress 3.0.0 (https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz) -275. requires-port 1.0.0 (https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz) -276. resolve-url 0.2.1 (https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz) -277. ret 0.1.15 (https://registry.npmjs.org/ret/-/ret-0.1.15.tgz) -278. roughjs-es5 0.1.0 (https://registry.npmjs.org/roughjs-es5/-/roughjs-es5-0.1.0.tgz) -279. rxjs 5.5.9 (https://registry.npmjs.org/rxjs/-/rxjs-5.5.9.tgz) -280. safe-buffer 5.1.2 (https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz) -281. safe-regex 1.1.0 (https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz) -282. safer-buffer 2.1.2 (https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz) -283. sax 1.2.4 (https://registry.npmjs.org/sax/-/sax-1.2.4.tgz) -284. schedule 0.5.0 (https://registry.npmjs.org/schedule/-/schedule-0.5.0.tgz) -285. semiotic 1.15.1 (https://registry.npmjs.org/semiotic/-/semiotic-1.15.1.tgz) -286. semiotic-mark 0.3.0 (https://registry.npmjs.org/semiotic-mark/-/semiotic-mark-0.3.0.tgz) -287. semver 5.5.0 (https://registry.npmjs.org/semver/-/semver-5.5.0.tgz) -288. set-value 2.0.0 (https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz) -289. setImmediate (for RxJS 5.5) (https://github.com/YuzuJS/setImmediate) -290. sizzle (for lodash 4.17) (https://sizzlejs.com/) -291. snapdragon 0.8.2 (https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz) -292. snapdragon-node 2.1.1 (https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz) -293. snapdragon-util 3.0.1 (https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz) -294. source-map 0.5.7 (https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz) -295. source-map-resolve 0.5.2 (https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz) -296. source-map-url 0.4.0 (https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz) -297. split-string 3.1.0 (https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz) -298. sshpk 1.14.1 (https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz) -299. state-toggle 1.0.1 (https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.1.tgz) -300. static-extend 0.1.2 (https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz) -301. string-hash 1.1.3 (https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz) -302. string_decoder 0.10.31 (https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz) -303. style-loader 0.23.1 (https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz) -304. styled-jsx 3.1.0 (https://registry.npmjs.org/styled-jsx/-/styled-jsx-3.1.0.tgz) -305. stylis-rule-sheet 0.0.10 (https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz) -306. sudo-prompt 8.2.0 (https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-8.2.0.tgz) -307. svg-path-bounding-box 1.0.4 (https://registry.npmjs.org/svg-path-bounding-box/-/svg-path-bounding-box-1.0.4.tgz) -308. svgpath 2.2.1 (https://registry.npmjs.org/svgpath/-/svgpath-2.2.1.tgz) -309. symbol-observable 1.0.1 (https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz) -310. throttleit 1.0.0 (https://github.com/component/throttle/tree/1.0.0) -311. tinycolor2 1.4.1 (https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz) -312. tinyqueue 1.2.3 (https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz) -313. tmp 0.0.29 (https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz) -314. to-object-path 0.3.0 (https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz) -315. to-regex 3.0.2 (https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz) -316. to-regex-range 2.1.1 (https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz) -317. tough-cookie 2.3.4 (https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz) -318. tree-kill 1.2.0 (https://github.com/pkrumins/node-tree-kill) -319. trim 0.0.1 (https://registry.npmjs.org/trim/-/trim-0.0.1.tgz) -320. trim-trailing-lines 1.1.1 (https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.1.tgz) -321. trough 1.0.3 (https://registry.npmjs.org/trough/-/trough-1.0.3.tgz) -322. tunnel-agent 0.6.0 (https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz) -323. tweetnacl 0.14.5 (https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz) -324. typescript-char 0.0.0 (https://github.com/mason-lang/typescript-char) -325. uint64be 1.0.1 (https://registry.npmjs.org/uint64be/-/uint64be-1.0.1.tgz) -326. ultron 1.0.2 (https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz) -327. underscore 1.8.3 (https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz) -328. unherit 1.1.1 (https://registry.npmjs.org/unherit/-/unherit-1.1.1.tgz) -329. unicode 10.0.0 (https://registry.npmjs.org/unicode/-/unicode-10.0.0.tgz) -330. unified 6.2.0 (https://registry.npmjs.org/unified/-/unified-6.2.0.tgz) -331. union-value 1.0.0 (https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz) -332. uniqid 5.0.3 (https://registry.npmjs.org/uniqid/-/uniqid-5.0.3.tgz) -333. unist-util-is 2.1.2 (https://registry.npmjs.org/unist-util-is/-/unist-util-is-2.1.2.tgz) -334. unist-util-remove-position 1.1.2 (https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.2.tgz) -335. unist-util-stringify-position 1.1.2 (https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz) -336. unist-util-visit 1.4.0 (https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.0.tgz) -337. unist-util-visit-parents 1.1.2 (https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz) -338. universalify 0.1.1 (https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz) -339. unset-value 1.0.0 (https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz) -340. untangle (for ptvsd 4) (https://pypi.org/project/untangle/) -341. untildify 3.0.2 (https://registry.npmjs.org/untildify/-/untildify-3.0.2.tgz) -342. urix 0.1.0 (https://registry.npmjs.org/urix/-/urix-0.1.0.tgz) -343. url-parse 1.4.3 (https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz) -344. use 3.1.0 (https://registry.npmjs.org/use/-/use-3.1.0.tgz) -345. util-deprecate 1.0.2 (https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz) -346. uuid 3.3.2 (https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz) -347. validator 9.4.1 (https://registry.npmjs.org/validator/-/validator-9.4.1.tgz) -348. verror 1.10.0 (https://registry.npmjs.org/verror/-/verror-1.10.0.tgz) -349. vfile 2.3.0 (https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz) -350. vfile-location 2.0.3 (https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.3.tgz) -351. vfile-message 1.0.1 (https://registry.npmjs.org/vfile-message/-/vfile-message-1.0.1.tgz) -352. viz-annotation 0.0.1-3 (https://registry.npmjs.org/viz-annotation/-/viz-annotation-0.0.1-3.tgz) -353. vscode-debugadapter 1.28.0 (https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.28.0.tgz) -354. vscode-debugprotocol 1.28.0 (https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.28.0.tgz) -355. vscode-extension-telemetry 0.1.0 (https://registry.npmjs.org/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.0.tgz) -356. vscode-jsonrpc 3.6.2 (https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-3.6.2.tgz) -357. vscode-languageclient 4.4.0 (https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-4.4.0.tgz) -358. vscode-languageserver 4.4.0 (https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-4.4.0.tgz) -359. vscode-languageserver-protocol 3.10.3 (https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.10.3.tgz) -360. vscode-languageserver-types 3.10.1 (https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.10.1.tgz) -361. vscode-uri 1.0.1 (https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.1.tgz) -362. webpack (for lodash 4) (https://webpack.js.org/) -363. winreg 1.2.4 (https://github.com/fresc81/node-winreg/tree/v1.2.4) -364. wrappy 1.0.2 (https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz) -365. ws 1.1.5 (https://registry.npmjs.org/ws/-/ws-1.1.5.tgz) -366. x-is-string 0.1.0 (https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz) -367. xml2js 0.4.19 (https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz) -368. xmlbuilder 9.0.7 (https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz) -369. xtend 4.0.1 (https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz) -370. zone.js 0.7.6 (https://registry.npmjs.org/zone.js/-/zone.js-0.7.6.tgz) - - -%% @babel/runtime-corejs2 7.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.1.2.tgz) -========================================= -MIT License - -Copyright (c) 2014-2018 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. - -========================================= -END OF @babel/runtime-corejs2 NOTICES AND INFORMATION - -%% @emotion/hash 0.6.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@emotion/hash/-/hash-0.6.6.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016 Kye Hohenberger - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 @emotion/hash NOTICES AND INFORMATION - -%% @emotion/memoize 0.6.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.6.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016 Kye Hohenberger - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 @emotion/memoize NOTICES AND INFORMATION - -%% @emotion/stylis 0.7.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@emotion/stylis/-/stylis-0.7.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016 Kye Hohenberger - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 @emotion/stylis NOTICES AND INFORMATION - -%% @emotion/unitless 0.6.7 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@emotion/unitless/-/unitless-0.6.7.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016 Kye Hohenberger - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 @emotion/unitless NOTICES AND INFORMATION - -%% @jupyterlab/coreutils 2.1.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-2.1.4.tgz) -========================================= -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. - -========================================= -END OF @jupyterlab/coreutils NOTICES AND INFORMATION - -%% @jupyterlab/observables 2.0.7 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@jupyterlab/observables/-/observables-2.0.7.tgz) -========================================= -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. - -========================================= -END OF @jupyterlab/observables NOTICES AND INFORMATION - -%% @jupyterlab/services 3.1.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@jupyterlab/services/-/services-3.1.4.tgz) -========================================= -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. - -========================================= -END OF @jupyterlab/services NOTICES AND INFORMATION - -%% @mapbox/polylabel 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@mapbox/polylabel/-/polylabel-1.0.2.tgz) -========================================= -ISC License -Copyright (c) 2016 Mapbox - -Permission to use, copy, modify, and/or distribute this software for any purpose -with or without fee is hereby granted, provided that the above copyright notice -and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND 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. - -========================================= -END OF @mapbox/polylabel NOTICES AND INFORMATION - -%% @nteract/markdown 2.1.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@nteract/markdown/-/markdown-2.1.4.tgz) -========================================= -Copyright (c) 2016, nteract contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of nteract nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -========================================= -END OF @nteract/markdown NOTICES AND INFORMATION - -%% @nteract/mathjax 2.1.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@nteract/mathjax/-/mathjax-2.1.4.tgz) -========================================= -Copyright (c) 2016, nteract contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of nteract nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -========================================= -END OF @nteract/mathjax NOTICES AND INFORMATION - -%% @nteract/octicons 0.4.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@nteract/octicons/-/octicons-0.4.3.tgz) -========================================= -Copyright (c) 2016, nteract contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of nteract nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -========================================= -END OF @nteract/octicons NOTICES AND INFORMATION - -%% @nteract/plotly 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@nteract/plotly/-/plotly-1.0.0.tgz) -========================================= -BSD 3-Clause License - -Copyright (c) 2017, nteract -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. - -========================================= -END OF @nteract/plotly NOTICES AND INFORMATION - -%% @nteract/transform-dataresource 4.3.5 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@nteract/transform-dataresource/-/transform-dataresource-4.3.5.tgz) -========================================= -Copyright (c) 2016, nteract contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of nteract nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -========================================= -END OF @nteract/transform-dataresource NOTICES AND INFORMATION - -%% @nteract/transform-geojson 3.2.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@nteract/transform-geojson/-/transform-geojson-3.2.3.tgz) -========================================= -Copyright (c) 2016, nteract contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of nteract nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -========================================= -END OF @nteract/transform-geojson NOTICES AND INFORMATION - -%% @nteract/transform-model-debug 3.2.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@nteract/transform-model-debug/-/transform-model-debug-3.2.3.tgz) -========================================= -Copyright (c) 2016, nteract contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of nteract nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -========================================= -END OF @nteract/transform-model-debug NOTICES AND INFORMATION - -%% @nteract/transform-plotly 3.2.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@nteract/transform-plotly/-/transform-plotly-3.2.3.tgz) -========================================= -Copyright (c) 2016, nteract contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of nteract nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -========================================= -END OF @nteract/transform-plotly NOTICES AND INFORMATION - -%% @nteract/transform-vdom 2.2.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@nteract/transform-vdom/-/transform-vdom-2.2.3.tgz) -========================================= -Copyright (c) 2016, nteract contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of nteract nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -========================================= -END OF @nteract/transform-vdom NOTICES AND INFORMATION - -%% @nteract/transforms 4.4.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@nteract/transforms/-/transforms-4.4.4.tgz) -========================================= -Copyright (c) 2016, nteract contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of nteract nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -========================================= -END OF @nteract/transforms NOTICES AND INFORMATION - -%% @phosphor/algorithm 1.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.1.2.tgz) -========================================= -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. - -========================================= -END OF @phosphor/algorithm NOTICES AND INFORMATION - -%% @phosphor/collections 1.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@phosphor/collections/-/collections-1.1.2.tgz) -========================================= -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. - - -========================================= -END OF @phosphor/collections NOTICES AND INFORMATION - -%% @phosphor/coreutils 1.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@phosphor/coreutils/-/coreutils-1.3.0.tgz) -========================================= -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. - - -========================================= -END OF @phosphor/coreutils NOTICES AND INFORMATION - -%% @phosphor/disposable 1.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.1.2.tgz) -========================================= -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. - -========================================= -END OF @phosphor/disposable NOTICES AND INFORMATION - -%% @phosphor/messaging 1.2.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@phosphor/messaging/-/messaging-1.2.2.tgz) -========================================= -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. - - -========================================= -END OF @phosphor/messaging NOTICES AND INFORMATION - -%% @phosphor/signaling 1.2.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.2.2.tgz) -========================================= -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. - - -========================================= -END OF @phosphor/signaling NOTICES AND INFORMATION - -%% _pydev_calltip_util.py (for PyDev.Debugger) NOTICES AND INFORMATION BEGIN HERE (https://github.com/fabioz/PyDev.Debugger/blob/master/_pydev_bundle/_pydev_calltip_util.py) -========================================= -Copyright (c) Yuli Fitterman - -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 _pydev_calltip_util.py NOTICES AND INFORMATION - -%% ajv 5.5.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 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. - - -========================================= -END OF ajv NOTICES AND INFORMATION - -%% angular.io (for RxJS 5.5) NOTICES AND INFORMATION BEGIN HERE (https://angular.io/) -========================================= -The MIT License - -Copyright (c) 2014-2017 Google, 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. - -========================================= -END OF angular.io NOTICES AND INFORMATION - -%% anser 1.4.7 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/anser/-/anser-1.4.7.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2012-18 Ionică Bizău (https://ionicabizau.net) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 anser NOTICES AND INFORMATION - -%% ansi-to-html 0.6.7 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.6.7.tgz) -========================================= -Copyright (c) 2012 Rob Burns - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION 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 ansi-to-html NOTICES AND INFORMATION - -%% ansi-to-react 3.3.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/ansi-to-react/-/ansi-to-react-3.3.3.tgz) -========================================= -Copyright (c) 2016, nteract contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of nteract nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -========================================= -END OF ansi-to-react NOTICES AND INFORMATION - -%% applicationinsights 1.0.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.0.6.tgz) -========================================= -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. - -========================================= -END OF applicationinsights NOTICES AND INFORMATION - -%% arch 2.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/arch/-/arch-2.1.0.tgz) -========================================= -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. - -========================================= -END OF arch NOTICES AND INFORMATION - -%% arr-diff 4.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF arr-diff NOTICES AND INFORMATION - -%% arr-flatten 1.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2017, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF arr-flatten NOTICES AND INFORMATION - -%% arr-union 3.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2016, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF arr-union NOTICES AND INFORMATION - -%% array-unique 0.3.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2016, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF array-unique NOTICES AND INFORMATION - -%% asn1 0.2.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz) -========================================= -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 - -========================================= -END OF asn1 NOTICES AND INFORMATION - -%% assert-plus 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://github.com/joyent/node-assert-plus/tree/v1.0.0) -========================================= -The MIT License (MIT) -Copyright (c) 2012 Mark Cavage - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 assert-plus NOTICES AND INFORMATION - -%% assign-symbols 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF assign-symbols NOTICES AND INFORMATION - -%% asynckit 0.4.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz) -========================================= -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. - -========================================= -END OF asynckit NOTICES AND INFORMATION - -%% atob 2.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/atob/-/atob-2.1.1.tgz) -========================================= -At your option you may choose either of the following licenses: - - * The MIT License (MIT) - * The Apache License 2.0 (Apache-2.0) - - -The MIT License (MIT) - -Copyright (c) 2015 AJ ONeal - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2015 AJ ONeal - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -========================================= -END OF atob NOTICES AND INFORMATION - -%% aws-sign2 0.7.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz) -========================================= -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 -========================================= -END OF aws-sign2 NOTICES AND INFORMATION - -%% aws4 1.7.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz) -========================================= -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. - -========================================= -END OF aws4 NOTICES AND INFORMATION - -%% azure-storage 2.10.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/azure-storage/-/azure-storage-2.10.1.tgz) -========================================= - 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 -========================================= -END OF azure-storage NOTICES AND INFORMATION - -%% babel-polyfill 6.26.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz) -========================================= -MIT License - -Copyright (c) 2014-2018 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. - - -========================================= -END OF babel-polyfill NOTICES AND INFORMATION - -%% babel-runtime 6.26.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz) -========================================= -MIT License - -Copyright (c) 2014-2018 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. - -========================================= -END OF babel-runtime NOTICES AND INFORMATION - -%% bail 1.0.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/bail/-/bail-1.0.3.tgz) -========================================= -(The MIT License) - -Copyright (c) 2015 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF bail NOTICES AND INFORMATION - -%% balanced-match 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz) -========================================= -(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. - -========================================= -END OF balanced-match NOTICES AND INFORMATION - -%% base 0.11.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/base/-/base-0.11.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2017, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF base NOTICES AND INFORMATION - -%% base16 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/base16/-/base16-1.0.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Dan Abramov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF base16 NOTICES AND INFORMATION - -%% bcrypt-pbkdf 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://www.npmjs.com/package/bcrypt-pbkdf) -========================================= -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 bcrypt-pbkdf NOTICES AND INFORMATION - -%% bintrees 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz) -========================================= -Copyright (C) 2011 by Vadim Graboys - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 bintrees NOTICES AND INFORMATION - -%% brace-expansion 1.1.11 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz) -========================================= -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. - -========================================= -END OF brace-expansion NOTICES AND INFORMATION - -%% braces 2.3.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/braces/-/braces-2.3.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2018, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF braces NOTICES AND INFORMATION - -%% browserify-mime 1.2.9 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/browserify-mime/-/browserify-mime-1.2.9.tgz) -========================================= -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. - -========================================= -END OF browserify-mime NOTICES AND INFORMATION - -%% cache-base 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2017, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -========================================= -END OF cache-base NOTICES AND INFORMATION - -%% caseless 0.12.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz) -========================================= -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 -========================================= -END OF caseless NOTICES AND INFORMATION - -%% character-entities-legacy 1.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.2.tgz) -========================================= -(The MIT License) - -Copyright (c) 2015 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF character-entities-legacy NOTICES AND INFORMATION - -%% character-reference-invalid 1.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.2.tgz) -========================================= -(The MIT License) - -Copyright (c) 2015 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF character-reference-invalid NOTICES AND INFORMATION - -%% charenc 0.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz) -========================================= -Copyright © 2011, Paul Vorbach. All rights reserved. -Copyright © 2009, Jeff Mott. All rights reserved. - -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. - -========================================= -END OF charenc NOTICES AND INFORMATION - -%% class-utils 0.3.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015, 2017-2018, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF class-utils NOTICES AND INFORMATION - -%% classnames 2.2.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz) -========================================= -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. - -========================================= -END OF classnames NOTICES AND INFORMATION - -%% co 4.6.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/co/-/co-4.6.0.tgz) -========================================= -(The MIT License) - -Copyright (c) 2014 TJ Holowaychuk <tj@vision-media.ca> - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF co NOTICES AND INFORMATION - -%% collapse-white-space 1.0.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.4.tgz) -========================================= -(The MIT License) - -Copyright (c) 2015 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF collapse-white-space NOTICES AND INFORMATION - -%% collection-visit 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015, 2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF collection-visit NOTICES AND INFORMATION - -%% combined-stream 1.0.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz) -========================================= -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. - -========================================= -END OF combined-stream NOTICES AND INFORMATION - -%% comment-json 1.1.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/comment-json/-/comment-json-1.1.3.tgz) -========================================= -Copyright (c) 2013 kaelzhang , contributors -http://kael.me/ - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -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 comment-json NOTICES AND INFORMATION - -%% component-emitter 1.2.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz) -========================================= -(The MIT License) - -Copyright (c) 2014 Component contributors - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF component-emitter NOTICES AND INFORMATION - -%% concat-map 0.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz) -========================================= -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. - -========================================= -END OF concat-map NOTICES AND INFORMATION - -%% copy-descriptor 0.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2016, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF copy-descriptor NOTICES AND INFORMATION - -%% core-js 2.5.7 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz) -========================================= -Copyright (c) 2014-2018 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. - -========================================= -END OF core-js NOTICES AND INFORMATION - -%% core-util-is 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz) -========================================= -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. - -========================================= -END OF core-util-is NOTICES AND INFORMATION - -%% create-emotion 9.2.12 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/create-emotion/-/create-emotion-9.2.12.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016 Kye Hohenberger - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 create-emotion NOTICES AND INFORMATION - -%% crypt 0.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz) -========================================= -Copyright © 2011, Paul Vorbach. All rights reserved. -Copyright © 2009, Jeff Mott. All rights reserved. - -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. - -========================================= -END OF crypt NOTICES AND INFORMATION - -%% css-loader 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/css-loader/-/css-loader-1.0.1.tgz) -========================================= -Copyright JS Foundation and other contributors - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF css-loader NOTICES AND INFORMATION - -%% d3-array 1.2.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-array NOTICES AND INFORMATION - -%% d3-bboxCollide 1.0.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-bboxCollide/-/d3-bboxCollide-1.0.4.tgz) -========================================= -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 - -========================================= -END OF d3-bboxCollide NOTICES AND INFORMATION - -%% d3-brush 1.0.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-brush/-/d3-brush-1.0.6.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-brush NOTICES AND INFORMATION - -%% d3-chord 1.0.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-chord NOTICES AND INFORMATION - -%% d3-collection 1.0.7 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz) -========================================= -Copyright 2010-2016, Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-collection NOTICES AND INFORMATION - -%% d3-color 1.2.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-color/-/d3-color-1.2.3.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-color NOTICES AND INFORMATION - -%% d3-contour 1.3.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz) -========================================= -Copyright 2012-2017 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-contour NOTICES AND INFORMATION - -%% d3-dispatch 1.0.5 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-dispatch NOTICES AND INFORMATION - -%% d3-drag 1.2.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.3.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-drag NOTICES AND INFORMATION - -%% d3-ease 1.0.5 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -Copyright 2001 Robert Penner -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-ease NOTICES AND INFORMATION - -%% d3-force 1.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-force/-/d3-force-1.1.2.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-force NOTICES AND INFORMATION - -%% d3-format 1.3.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz) -========================================= -Copyright 2010-2015 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-format NOTICES AND INFORMATION - -%% d3-glyphedge 1.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-glyphedge/-/d3-glyphedge-1.2.0.tgz) -========================================= -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 - - -========================================= -END OF d3-glyphedge NOTICES AND INFORMATION - -%% d3-hexbin 0.2.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-hexbin/-/d3-hexbin-0.2.2.tgz) -========================================= -Copyright Mike Bostock, 2012-2016 -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-hexbin NOTICES AND INFORMATION - -%% d3-hierarchy 1.1.8 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-hierarchy NOTICES AND INFORMATION - -%% d3-interpolate 1.3.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-interpolate NOTICES AND INFORMATION - -%% d3-path 1.0.7 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-path/-/d3-path-1.0.7.tgz) -========================================= -Copyright 2015-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-path NOTICES AND INFORMATION - -%% d3-polygon 1.0.5 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-polygon NOTICES AND INFORMATION - -%% d3-quadtree 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.1.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-quadtree NOTICES AND INFORMATION - -%% d3-sankey-circular 0.25.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-sankey-circular/-/d3-sankey-circular-0.25.0.tgz) -========================================= -MIT License - -Copyright (c) 2017 Tom Shanley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 d3-sankey-circular NOTICES AND INFORMATION - -%% d3-scale 1.0.7 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-scale/-/d3-scale-1.0.7.tgz) -========================================= -Copyright 2010-2015 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-scale NOTICES AND INFORMATION - -%% d3-selection 1.3.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-selection/-/d3-selection-1.3.2.tgz) -========================================= -Copyright (c) 2010-2018, Michael Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* The name Michael Bostock may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-selection NOTICES AND INFORMATION - -%% d3-shape 1.2.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-shape/-/d3-shape-1.2.2.tgz) -========================================= -Copyright 2010-2015 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-shape NOTICES AND INFORMATION - -%% d3-time 1.0.10 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-time/-/d3-time-1.0.10.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-time NOTICES AND INFORMATION - -%% d3-time-format 2.1.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz) -========================================= -Copyright 2010-2017 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-time-format NOTICES AND INFORMATION - -%% d3-timer 1.0.9 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-timer NOTICES AND INFORMATION - -%% d3-transition 1.1.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-transition/-/d3-transition-1.1.3.tgz) -========================================= -Copyright (c) 2010-2015, Michael Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* The name Michael Bostock may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -TERMS OF USE - EASING EQUATIONS - -Open source under the BSD License. - -Copyright 2001 Robert Penner -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -- Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -- Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -- Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF d3-transition NOTICES AND INFORMATION - -%% d3-voronoi 1.1.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz) -========================================= -Copyright 2010-2016 Mike Bostock -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the author nor the names of contributors may be used to - endorse or promote products derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -Copyright (C) 2010-2013 Raymond Hill -https://github.com/gorhill/Javascript-Voronoi - -Licensed under The MIT License -http://en.wikipedia.org/wiki/MIT_License - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF d3-voronoi NOTICES AND INFORMATION - -%% dashdash 1.14.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz) -========================================= -# 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. - - -========================================= -END OF dashdash NOTICES AND INFORMATION - -%% debug 2.6.9 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/debug/-/debug-2.6.9.tgz) -========================================= -(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. - - -========================================= -END OF debug NOTICES AND INFORMATION - -%% decode-uri-component 0.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz) -========================================= -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. - -========================================= -END OF decode-uri-component NOTICES AND INFORMATION - -%% define-property 2.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2018, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF define-property NOTICES AND INFORMATION - -%% delayed-stream 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz) -========================================= -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. - -========================================= -END OF delayed-stream NOTICES AND INFORMATION - -%% diagnostic-channel 0.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz) -========================================= - 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 - -========================================= -END OF diagnostic-channel NOTICES AND INFORMATION - -%% diagnostic-channel-publishers 0.2.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz) -========================================= - 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 - -========================================= -END OF diagnostic-channel-publishers NOTICES AND INFORMATION - -%% diff-match-patch 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.0.tgz) -========================================= -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 diff-match-patch NOTICES AND INFORMATION - -%% dotenv 5.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz) -========================================= -Copyright (c) 2015, Scott Motte -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF dotenv NOTICES AND INFORMATION - -%% ecc-jsbn 0.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz) -========================================= -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. -========================================= -END OF ecc-jsbn NOTICES AND INFORMATION - -%% emotion 9.2.12 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/emotion/-/emotion-9.2.12.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016 Kye Hohenberger - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 emotion NOTICES AND INFORMATION - -%% encoding 0.1.12 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz) -========================================= -Copyright (c) 2012-2014 Andris Reinman - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF encoding NOTICES AND INFORMATION - -%% entities 1.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/entities/-/entities-1.1.1.tgz) -========================================= -Copyright (c) Felix Böhm -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF entities NOTICES AND INFORMATION - -%% escape-carriage 1.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.2.0.tgz) -========================================= -MIT License - -Copyright (c) 2016 Lukas Geiger - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 escape-carriage NOTICES AND INFORMATION - -%% esprima 2.7.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz) -========================================= -Copyright (c) jQuery Foundation, Inc. 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. - -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. - -========================================= -END OF esprima NOTICES AND INFORMATION - -%% expand-brackets 2.1.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2016, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF expand-brackets NOTICES AND INFORMATION - -%% extend 3.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/extend/-/extend-3.0.1.tgz) -========================================= -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. - - -========================================= -END OF extend NOTICES AND INFORMATION - -%% extend-shallow 3.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2015, 2017, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF extend-shallow NOTICES AND INFORMATION - -%% extglob 2.0.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2017, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF extglob NOTICES AND INFORMATION - -%% extsprintf 1.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz) -========================================= -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 - -========================================= -END OF extsprintf NOTICES AND INFORMATION - -%% fast-deep-equal 1.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz) -========================================= -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. - -========================================= -END OF fast-deep-equal NOTICES AND INFORMATION - -%% fast-json-stable-stringify 2.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz) -========================================= -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. - -========================================= -END OF fast-json-stable-stringify NOTICES AND INFORMATION - -%% fbjs 0.8.17 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz) -========================================= -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. - -========================================= -END OF fbjs NOTICES AND INFORMATION - -%% file-matcher 1.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/file-matcher/-/file-matcher-1.3.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2017 Mauricio Gemelli Vigolo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the 'Software'), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 file-matcher NOTICES AND INFORMATION - -%% fill-range 4.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF fill-range NOTICES AND INFORMATION - -%% flat 4.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/flat/-/flat-4.0.0.tgz) -========================================= -Copyright (c) 2014, Hugh Kennedy -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 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 flat NOTICES AND INFORMATION - -%% for-in 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF for-in NOTICES AND INFORMATION - -%% forever-agent 0.6.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz) -========================================= -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 -========================================= -END OF forever-agent NOTICES AND INFORMATION - -%% form-data 2.3.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz) -========================================= -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. - -========================================= -END OF form-data NOTICES AND INFORMATION - -%% fragment-cache 0.2.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016-2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF fragment-cache NOTICES AND INFORMATION - -%% fs-extra 4.0.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz) -========================================= -(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. - -========================================= -END OF fs-extra NOTICES AND INFORMATION - -%% fs.realpath 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz) -========================================= -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. - -========================================= -END OF fs.realpath NOTICES AND INFORMATION - -%% fuzzy 0.1.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz) -========================================= -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. - -========================================= -END OF fuzzy NOTICES AND INFORMATION - -%% get-port 3.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz) -========================================= -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. - -========================================= -END OF get-port NOTICES AND INFORMATION - -%% get-value 2.0.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2016, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF get-value NOTICES AND INFORMATION - -%% getpass 0.1.7 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz) -========================================= -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. - -========================================= -END OF getpass NOTICES AND INFORMATION - -%% glob 7.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/glob/-/glob-7.1.2.tgz) -========================================= -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. - -========================================= -END OF glob NOTICES AND INFORMATION - -%% graceful-fs 4.1.11 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz) -========================================= -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. - -========================================= -END OF graceful-fs NOTICES AND INFORMATION - -%% har-schema 2.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz) -========================================= -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. - -========================================= -END OF har-schema NOTICES AND INFORMATION - -%% har-validator 5.0.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz) -========================================= -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. - -========================================= -END OF har-validator NOTICES AND INFORMATION - -%% has-value 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF has-value NOTICES AND INFORMATION - -%% has-values 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF has-values NOTICES AND INFORMATION - -%% hash-base 3.0.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz) -========================================= -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. - -========================================= -END OF hash-base NOTICES AND INFORMATION - -%% http-signature 1.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz) -========================================= -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. - -========================================= -END OF http-signature NOTICES AND INFORMATION - -%% iconv-lite 0.4.21 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz) -========================================= -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. - - -========================================= -END OF iconv-lite NOTICES AND INFORMATION - -%% inflight 1.0.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz) -========================================= -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. - -========================================= -END OF inflight NOTICES AND INFORMATION - -%% inherits 2.0.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz) -========================================= -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. - - -========================================= -END OF inherits NOTICES AND INFORMATION - -%% inversify 4.11.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/inversify/-/inversify-4.11.1.tgz) -========================================= -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. - - -========================================= -END OF inversify NOTICES AND INFORMATION - -%% IPython (for PyDev.Debugger) NOTICES AND INFORMATION BEGIN HERE (https://ipython.org/) -========================================= -Copyright (c) 2008-2010, IPython Development Team -Copyright (c) 2001-2007, Fernando Perez. -Copyright (c) 2001, Janko Hauser -Copyright (c) 2001, Nathaniel Gray - -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 IPython 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. - -========================================= -END OF IPython NOTICES AND INFORMATION - -%% is-accessor-descriptor 0.1.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF is-accessor-descriptor NOTICES AND INFORMATION - -%% is-alphabetical 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.2.tgz) -========================================= -(The MIT License) - -Copyright (c) 2016 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF is-alphabetical NOTICES AND INFORMATION - -%% is-alphanumerical 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.2.tgz) -========================================= -(The MIT License) - -Copyright (c) 2016 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF is-alphanumerical NOTICES AND INFORMATION - -%% is-buffer 1.1.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz) -========================================= -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. - -========================================= -END OF is-buffer NOTICES AND INFORMATION - -%% is-data-descriptor 0.1.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF is-data-descriptor NOTICES AND INFORMATION - -%% is-decimal 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.2.tgz) -========================================= -(The MIT License) - -Copyright (c) 2016 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF is-decimal NOTICES AND INFORMATION - -%% is-descriptor 0.1.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2017, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -========================================= -END OF is-descriptor NOTICES AND INFORMATION - -%% is-extendable 0.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF is-extendable NOTICES AND INFORMATION - -%% is-hexadecimal 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz) -========================================= -(The MIT License) - -Copyright (c) 2016 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF is-hexadecimal NOTICES AND INFORMATION - -%% is-number 3.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2016, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF is-number NOTICES AND INFORMATION - -%% is-odd 2.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2017, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF is-odd NOTICES AND INFORMATION - -%% is-plain-obj 1.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz) -========================================= -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. - -========================================= -END OF is-plain-obj NOTICES AND INFORMATION - -%% is-plain-object 2.0.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2017, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF is-plain-object NOTICES AND INFORMATION - -%% is-stream 1.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz) -========================================= -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. - -========================================= -END OF is-stream NOTICES AND INFORMATION - -%% is-typedarray 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz) -========================================= -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. - -========================================= -END OF is-typedarray NOTICES AND INFORMATION - -%% is-whitespace-character 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.2.tgz) -========================================= -(The MIT License) - -Copyright (c) 2016 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF is-whitespace-character NOTICES AND INFORMATION - -%% is-windows 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2018, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF is-windows NOTICES AND INFORMATION - -%% is-word-character 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.2.tgz) -========================================= -(The MIT License) - -Copyright (c) 2016 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF is-word-character NOTICES AND INFORMATION - -%% is-wsl 1.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz) -========================================= -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. - -========================================= -END OF is-wsl NOTICES AND INFORMATION - -%% isarray 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://github.com/juliangruber/isarray/blob/v1.0.0) -========================================= -(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. - -========================================= -END OF isarray NOTICES AND INFORMATION - -%% isobject 3.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2017, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -========================================= -END OF isobject NOTICES AND INFORMATION - -%% isort 4.3.4 NOTICES AND INFORMATION BEGIN HERE (https://github.com/timothycrosley/isort/tree/4.3.4) -========================================= -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. - -========================================= -END OF isort NOTICES AND INFORMATION - -%% isstream 0.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz) -========================================= -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. - -========================================= -END OF isstream NOTICES AND INFORMATION - -%% Jedi 0.12.0 NOTICES AND INFORMATION BEGIN HERE (https://github.com/davidhalter/jedi/tree/v0.12.0) -========================================= -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. - -========================================= -END OF Jedi NOTICES AND INFORMATION - -%% jsbn 0.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz) -========================================= -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 -========================================= -END OF jsbn NOTICES AND INFORMATION - -%% json-edm-parser 0.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/json-edm-parser/-/json-edm-parser-0.1.2.tgz) -========================================= -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. - - -========================================= -END OF json-edm-parser NOTICES AND INFORMATION - -%% json-parser 1.1.5 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/json-parser/-/json-parser-1.1.5.tgz) -========================================= -Copyright (c) 2013 kaelzhang , contributors -http://kael.me/ - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -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 json-parser NOTICES AND INFORMATION - -%% json-schema 0.2.3 NOTICES AND INFORMATION BEGIN HERE (https://www.npmjs.com/package/json-schema) -========================================= -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 json-schema NOTICES AND INFORMATION - -%% json-schema-traverse 0.3.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz) -========================================= -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. - -========================================= -END OF json-schema-traverse NOTICES AND INFORMATION - -%% json-stable-stringify 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz) -========================================= -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. - -========================================= -END OF json-stable-stringify NOTICES AND INFORMATION - -%% json-stringify-safe 5.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz) -========================================= -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. - -========================================= -END OF json-stringify-safe NOTICES AND INFORMATION - -%% json2csv 3.11.5 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/json2csv/-/json2csv-3.11.5.tgz) -========================================= -Copyright (C) 2012 [Mirco Zeiss](mailto: mirco.zeiss@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. - -========================================= -END OF json2csv NOTICES AND INFORMATION - -%% jsonfile 4.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz) -========================================= -(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. - -========================================= -END OF jsonfile NOTICES AND INFORMATION - -%% jsonify 0.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz) -========================================= -public domain - -========================================= -END OF jsonify NOTICES AND INFORMATION - -%% jsonparse 1.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz) -========================================= -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. - -========================================= -END OF jsonparse NOTICES AND INFORMATION - -%% jsprim 1.4.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz) -========================================= -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 - -========================================= -END OF jsprim NOTICES AND INFORMATION - -%% kind-of 6.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2017, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF kind-of NOTICES AND INFORMATION - -%% labella 1.1.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/labella/-/labella-1.1.4.tgz) -========================================= -Copyright 2015 Twitter, Inc. - -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 labella NOTICES AND INFORMATION - -%% leaflet 1.3.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/leaflet/-/leaflet-1.3.4.tgz) -========================================= -Copyright (c) 2010-2018, Vladimir Agafonkin -Copyright (c) 2010-2011, CloudMade -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. - -========================================= -END OF leaflet NOTICES AND INFORMATION - -%% line-by-line 0.1.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/line-by-line/-/line-by-line-0.1.6.tgz) -========================================= - -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. - -========================================= -END OF line-by-line NOTICES AND INFORMATION - -%% lodash 4.17.11 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz) -========================================= -Copyright JS 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. - -========================================= -END OF lodash NOTICES AND INFORMATION - -%% lodash.clonedeep 4.5.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz) -========================================= -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. - -========================================= -END OF lodash.clonedeep NOTICES AND INFORMATION - -%% lodash.curry 4.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz) -========================================= -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. - -========================================= -END OF lodash.curry NOTICES AND INFORMATION - -%% lodash.flatten 4.4.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz) -========================================= -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. - -========================================= -END OF lodash.flatten NOTICES AND INFORMATION - -%% lodash.flow 3.5.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz) -========================================= -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. - -========================================= -END OF lodash.flow NOTICES AND INFORMATION - -%% lodash.get 4.4.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz) -========================================= -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. - -========================================= -END OF lodash.get NOTICES AND INFORMATION - -%% lodash.set 4.3.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz) -========================================= -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. - -========================================= -END OF lodash.set NOTICES AND INFORMATION - -%% lodash.uniq 4.5.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz) -========================================= -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. - -========================================= -END OF lodash.uniq NOTICES AND INFORMATION - -%% map-cache 0.2.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2016, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF map-cache NOTICES AND INFORMATION - -%% map-visit 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF map-visit NOTICES AND INFORMATION - -%% markdown-escapes 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.2.tgz) -========================================= -(The MIT License) - -Copyright (c) 2016 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF markdown-escapes NOTICES AND INFORMATION - -%% martinez-polygon-clipping 0.1.5 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.1.5.tgz) -========================================= -MIT License - -Copyright (c) 2018 Alexander Milevski - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 martinez-polygon-clipping NOTICES AND INFORMATION - -%% material-colors 1.2.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz) -========================================= -ISC License - -Copyright 2014 Shuhei Kagawa - -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. - -========================================= -END OF material-colors NOTICES AND INFORMATION - -%% md5 2.2.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/md5/-/md5-2.2.1.tgz) -========================================= -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. - -========================================= -END OF md5 NOTICES AND INFORMATION - -%% md5.js 1.3.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz) -========================================= -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. - -========================================= -END OF md5.js NOTICES AND INFORMATION - -%% mdast-add-list-metadata 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2018 André Staltz (staltz.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. - -========================================= -END OF mdast-add-list-metadata NOTICES AND INFORMATION - -%% memoize-one 4.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.0.tgz) -========================================= -MIT License - -Copyright (c) 2017 Alexander Reardon - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -========================================= -END OF memoize-one NOTICES AND INFORMATION - -%% micromatch 3.1.10 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2018, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF micromatch NOTICES AND INFORMATION - -%% mime-db 1.33.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz) -========================================= - -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. - -========================================= -END OF mime-db NOTICES AND INFORMATION - -%% mime-types 2.1.18 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz) -========================================= -(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. - -========================================= -END OF mime-types NOTICES AND INFORMATION - -%% minimatch 3.0.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz) -========================================= -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. - -========================================= -END OF minimatch NOTICES AND INFORMATION - -%% minimist 1.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz) -========================================= -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. - -========================================= -END OF minimist NOTICES AND INFORMATION - -%% mixin-deep 1.3.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2015, 2017, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF mixin-deep NOTICES AND INFORMATION - -%% moment 2.21.0 NOTICES AND INFORMATION BEGIN HERE (http://registry.npmjs.org/moment/-/moment-2.21.0.tgz) -========================================= -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. - -========================================= -END OF moment NOTICES AND INFORMATION - -%% ms 2.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/ms/-/ms-2.0.0.tgz) -========================================= -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. - -========================================= -END OF ms NOTICES AND INFORMATION - -%% named-js-regexp 1.3.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/named-js-regexp/-/named-js-regexp-1.3.3.tgz) -========================================= -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. -========================================= -END OF named-js-regexp NOTICES AND INFORMATION - -%% nanomatch 1.2.9 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016-2018, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF nanomatch NOTICES AND INFORMATION - -%% node-fetch 1.7.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz) -========================================= -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. - - -========================================= -END OF node-fetch NOTICES AND INFORMATION - -%% node-stream-zip 1.6.0 NOTICES AND INFORMATION BEGIN HERE (https://github.com/antelle/node-stream-zip/tree/1.6.0) -========================================= -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. - -========================================= -END OF node-stream-zip NOTICES AND INFORMATION - -%% numeral 2.0.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz) -========================================= -Copyright (c) 2016 Adam Draper - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION 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 numeral NOTICES AND INFORMATION - -%% oauth-sign 0.8.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz) -========================================= -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 -========================================= -END OF oauth-sign NOTICES AND INFORMATION - -%% object-assign 4.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz) -========================================= -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. - -========================================= -END OF object-assign NOTICES AND INFORMATION - -%% object-copy 0.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF object-copy NOTICES AND INFORMATION - -%% object-visit 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015, 2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF object-visit NOTICES AND INFORMATION - -%% object.pick 1.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2016, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF object.pick NOTICES AND INFORMATION - -%% once 1.4.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/once/-/once-1.4.0.tgz) -========================================= -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. - -========================================= -END OF once NOTICES AND INFORMATION - -%% opn 5.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/opn/-/opn-5.3.0.tgz) -========================================= -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. - -========================================= -END OF opn NOTICES AND INFORMATION - -%% options 0.0.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/options/-/options-0.0.6.tgz) -========================================= -(The MIT License) - -Copyright (c) 2012 Einar Otto Stangvik <einaros@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. - -========================================= -END OF options NOTICES AND INFORMATION - -%% os-browserify 0.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2017 CoderPuppy - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 os-browserify NOTICES AND INFORMATION - -%% os-tmpdir 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz) -========================================= -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. - -========================================= -END OF os-tmpdir NOTICES AND INFORMATION - -%% parse-entities 1.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.0.tgz) -========================================= -(The MIT License) - -Copyright (c) 2015 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF parse-entities NOTICES AND INFORMATION - -%% parso 0.2.1 NOTICES AND INFORMATION BEGIN HERE (https://github.com/davidhalter/parso/tree/v0.2.1) -========================================= -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. - -========================================= -END OF parso NOTICES AND INFORMATION - -%% pascalcase 0.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF pascalcase NOTICES AND INFORMATION - -%% path-browserify 0.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz) -========================================= -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. - -========================================= -END OF path-browserify NOTICES AND INFORMATION - -%% path-is-absolute 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz) -========================================= -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. - -========================================= -END OF path-is-absolute NOTICES AND INFORMATION - -%% path-posix 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz) -========================================= -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. - -==== - -========================================= -END OF path-posix NOTICES AND INFORMATION - -%% performance-now 2.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz) -========================================= -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. -========================================= -END OF performance-now NOTICES AND INFORMATION - -%% pidusage 1.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/pidusage/-/pidusage-1.2.0.tgz) -========================================= -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. -========================================= -END OF pidusage NOTICES AND INFORMATION - -%% polygon-offset 0.3.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/polygon-offset/-/polygon-offset-0.3.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014 Alexander Milevski - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION 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 polygon-offset NOTICES AND INFORMATION - -%% posix-character-classes 0.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016-2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF posix-character-classes NOTICES AND INFORMATION - -%% prismjs 1.15.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/prismjs/-/prismjs-1.15.0.tgz) -========================================= -MIT LICENSE - -Copyright (c) 2012 Lea Verou - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 prismjs NOTICES AND INFORMATION - -%% process 0.11.10 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/process/-/process-0.11.10.tgz) -========================================= -(The MIT License) - -Copyright (c) 2013 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. - -========================================= -END OF process NOTICES AND INFORMATION - -%% process-nextick-args 1.0.7 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz) -========================================= -# 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.** - -========================================= -END OF process-nextick-args NOTICES AND INFORMATION - -%% prop-types 15.6.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz) -========================================= -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. - -========================================= -END OF prop-types NOTICES AND INFORMATION - -%% psl 1.1.29 NOTICES AND INFORMATION BEGIN HERE (https://github.com/wrangr/psl/tree/v1.1.29) -========================================= -The MIT License (MIT) - -Copyright (c) 2017 Lupo Montero - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 psl NOTICES AND INFORMATION - -%% ptvsd 4.2.0 NOTICES AND INFORMATION BEGIN HERE (https://github.com/Microsoft/ptvsd/tree/v4.2.0) -========================================= - ptvsd - - Copyright (c) Microsoft Corporation - All rights reserved. - - MIT License - - Permission is hereby granted, free of charge, to any person obtaining a copy of - this software and associated documentation files (the "Software"), to deal in - the Software without restriction, including without limitation the rights to - use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - the Software, and to permit persons to whom the Software is furnished to do so, - subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF ptvsd NOTICES AND INFORMATION - -%% punycode 1.4.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz) -========================================= -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. - -========================================= -END OF punycode NOTICES AND INFORMATION - -%% pure-color 1.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Nick Williams -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. - - -========================================= -END OF pure-color NOTICES AND INFORMATION - -%% py2app (for PyDev.Debugger) NOTICES AND INFORMATION BEGIN HERE (https://bitbucket.org/ronaldoussoren/py2app) -========================================= -This is the MIT license. This software may also be distributed under the same terms as Python (the PSF license). - -Copyright (c) 2004 Bob Ippolito. - -Some parts copyright (c) 2010-2014 Ronald Oussoren - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 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 py2app NOTICES AND INFORMATION - -%% PyDev.Debugger (for ptvsd 4) NOTICES AND INFORMATION BEGIN HERE (https://pypi.org/project/pydevd/) -========================================= -Eclipse Public License - v 1.0 - -THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC -LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM -CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - -a) in the case of the initial Contributor, the initial code and documentation - distributed under this Agreement, and -b) in the case of each subsequent Contributor: - i) changes to the Program, and - ii) additions to the Program; - - where such changes and/or additions to the Program originate from and are - distributed by that particular Contributor. A Contribution 'originates' - from a Contributor if it was added to the Program by such Contributor - itself or anyone acting on such Contributor's behalf. Contributions do not - include additions to the Program which: (i) are separate modules of - software distributed in conjunction with the Program under their own - license agreement, and (ii) are not derivative works of the Program. - -"Contributor" means any person or entity that distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which are -necessarily infringed by the use or sale of its Contribution alone or when -combined with the Program. - -"Program" means the Contributions distributed in accordance with this -Agreement. - -"Recipient" means anyone who receives the Program under this Agreement, -including all Contributors. - -2. GRANT OF RIGHTS - a) Subject to the terms of this Agreement, each Contributor hereby grants - Recipient a non-exclusive, worldwide, royalty-free copyright license to - reproduce, prepare derivative works of, publicly display, publicly - perform, distribute and sublicense the Contribution of such Contributor, - if any, and such derivative works, in source code and object code form. - b) Subject to the terms of this Agreement, each Contributor hereby grants - Recipient a non-exclusive, worldwide, royalty-free patent license under - Licensed Patents to make, use, sell, offer to sell, import and otherwise - transfer the Contribution of such Contributor, if any, in source code and - object code form. This patent license shall apply to the combination of - the Contribution and the Program if, at the time the Contribution is - added by the Contributor, such addition of the Contribution causes such - combination to be covered by the Licensed Patents. The patent license - shall not apply to any other combinations which include the Contribution. - No hardware per se is licensed hereunder. - c) Recipient understands that although each Contributor grants the licenses - to its Contributions set forth herein, no assurances are provided by any - Contributor that the Program does not infringe the patent or other - intellectual property rights of any other entity. Each Contributor - disclaims any liability to Recipient for claims brought by any other - entity based on infringement of intellectual property rights or - otherwise. As a condition to exercising the rights and licenses granted - hereunder, each Recipient hereby assumes sole responsibility to secure - any other intellectual property rights needed, if any. For example, if a - third party patent license is required to allow Recipient to distribute - the Program, it is Recipient's responsibility to acquire that license - before distributing the Program. - d) Each Contributor represents that to its knowledge it has sufficient - copyright rights in its Contribution, if any, to grant the copyright - license set forth in this Agreement. - -3. REQUIREMENTS - -A Contributor may choose to distribute the Program in object code form under -its own license agreement, provided that: - - a) it complies with the terms and conditions of this Agreement; and - b) its license agreement: - i) effectively disclaims on behalf of all Contributors all warranties - and conditions, express and implied, including warranties or - conditions of title and non-infringement, and implied warranties or - conditions of merchantability and fitness for a particular purpose; - ii) effectively excludes on behalf of all Contributors all liability for - damages, including direct, indirect, special, incidental and - consequential damages, such as lost profits; - iii) states that any provisions which differ from this Agreement are - offered by that Contributor alone and not by any other party; and - iv) states that source code for the Program is available from such - Contributor, and informs licensees how to obtain it in a reasonable - manner on or through a medium customarily used for software exchange. - -When the Program is made available in source code form: - - a) it must be made available under this Agreement; and - b) a copy of this Agreement must be included with each copy of the Program. - Contributors may not remove or alter any copyright notices contained - within the Program. - -Each Contributor must identify itself as the originator of its Contribution, -if -any, in a manner that reasonably allows subsequent Recipients to identify the -originator of the Contribution. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities with -respect to end users, business partners and the like. While this license is -intended to facilitate the commercial use of the Program, the Contributor who -includes the Program in a commercial product offering should do so in a manner -which does not create potential liability for other Contributors. Therefore, -if a Contributor includes the Program in a commercial product offering, such -Contributor ("Commercial Contributor") hereby agrees to defend and indemnify -every other Contributor ("Indemnified Contributor") against any losses, -damages and costs (collectively "Losses") arising from claims, lawsuits and -other legal actions brought by a third party against the Indemnified -Contributor to the extent caused by the acts or omissions of such Commercial -Contributor in connection with its distribution of the Program in a commercial -product offering. The obligations in this section do not apply to any claims -or Losses relating to any actual or alleged intellectual property -infringement. In order to qualify, an Indemnified Contributor must: -a) promptly notify the Commercial Contributor in writing of such claim, and -b) allow the Commercial Contributor to control, and cooperate with the -Commercial Contributor in, the defense and any related settlement -negotiations. The Indemnified Contributor may participate in any such claim at -its own expense. - -For example, a Contributor might include the Program in a commercial product -offering, Product X. That Contributor is then a Commercial Contributor. If -that Commercial Contributor then makes performance claims, or offers -warranties related to Product X, those performance claims and warranties are -such Commercial Contributor's responsibility alone. Under this section, the -Commercial Contributor would have to defend claims against the other -Contributors related to those performance claims and warranties, and if a -court requires any other Contributor to pay any damages as a result, the -Commercial Contributor must pay those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED 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. Each -Recipient is solely responsible for determining the appropriateness of using -and distributing the Program and assumes all risks associated with its -exercise of rights under this Agreement , including but not limited to the -risks and costs of program errors, compliance with applicable laws, damage to -or loss of data, programs or equipment, and unavailability or interruption of -operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY -CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION -LOST PROFITS), 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 OR DISTRIBUTION OF THE PROGRAM OR THE -EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY -OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under -applicable law, it shall not affect the validity or enforceability of the -remainder of the terms of this Agreement, and without further action by the -parties hereto, such provision shall be reformed to the minimum extent -necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Program itself -(excluding combinations of the Program with other software or hardware) -infringes such Recipient's patent(s), then such Recipient's rights granted -under Section 2(b) shall terminate as of the date such litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it fails to -comply with any of the material terms or conditions of this Agreement and does -not cure such failure in a reasonable period of time after becoming aware of -such noncompliance. If all Recipient's rights under this Agreement terminate, -Recipient agrees to cease use and distribution of the Program as soon as -reasonably practicable. However, Recipient's obligations under this Agreement -and any licenses granted by Recipient relating to the Program shall continue -and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, but in -order to avoid inconsistency the Agreement is copyrighted and may only be -modified in the following manner. The Agreement Steward reserves the right to -publish new versions (including revisions) of this Agreement from time to -time. No one other than the Agreement Steward has the right to modify this -Agreement. The Eclipse Foundation is the initial Agreement Steward. The -Eclipse Foundation may assign the responsibility to serve as the Agreement -Steward to a suitable separate entity. Each new version of the Agreement will -be given a distinguishing version number. The Program (including -Contributions) may always be distributed subject to the version of the -Agreement under which it was received. In addition, after a new version of the -Agreement is published, Contributor may elect to distribute the Program -(including its Contributions) under the new version. Except as expressly -stated in Sections 2(a) and 2(b) above, Recipient receives no rights or -licenses to the intellectual property of any Contributor under this Agreement, -whether expressly, by implication, estoppel or otherwise. All rights in the -Program not expressly granted under this Agreement are reserved. - -This Agreement is governed by the laws of the State of New York and the -intellectual property laws of the United States of America. No party to this -Agreement will bring a legal action under this Agreement more than one year -after the cause of action arose. Each party waives its rights to a jury trial in -any resulting litigation. - -========================================= -END OF PyDev.Debugger NOTICES AND INFORMATION - -%% qs 6.5.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/qs/-/qs-6.5.2.tgz) -========================================= -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 - -========================================= -END OF qs NOTICES AND INFORMATION - -%% querystringify 2.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz) -========================================= -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. - - -========================================= -END OF querystringify NOTICES AND INFORMATION - -%% react 16.5.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react/-/react-16.5.2.tgz) -========================================= -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. - -========================================= -END OF react NOTICES AND INFORMATION - -%% react-annotation 1.3.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-annotation/-/react-annotation-1.3.1.tgz) -========================================= - - 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) 2017, Susie Lu - - 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 react-annotation NOTICES AND INFORMATION - -%% react-base16-styling 0.5.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.5.3.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016 Alexander Kuznetsov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF react-base16-styling NOTICES AND INFORMATION - -%% react-color 2.14.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-color/-/react-color-2.14.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Case Sandberg - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 react-color NOTICES AND INFORMATION - -%% react-dom 16.5.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-dom/-/react-dom-16.5.2.tgz) -========================================= -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. - -========================================= -END OF react-dom NOTICES AND INFORMATION - -%% react-hot-loader 4.3.11 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.3.11.tgz) -========================================= -MIT License - -Copyright (c) 2016 Dan Abramov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF react-hot-loader NOTICES AND INFORMATION - -%% react-json-tree 0.11.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.11.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Shusaku Uesugi, (c) 2016-present Alexander Kuznetsov - - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF react-json-tree NOTICES AND INFORMATION - -%% react-markdown 3.6.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-markdown/-/react-markdown-3.6.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Espen Hovlandsdal - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF react-markdown NOTICES AND INFORMATION - -%% react-table 6.8.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-table/-/react-table-6.8.6.tgz) -========================================= -MIT License - -Copyright (c) 2016 Tanner Linsley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF react-table NOTICES AND INFORMATION - -%% react-table-hoc-fixed-columns 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-table-hoc-fixed-columns/-/react-table-hoc-fixed-columns-1.0.1.tgz) -========================================= -MIT License - -Copyright (c) 2018 Guillaume Jasmin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 react-table-hoc-fixed-columns NOTICES AND INFORMATION - -%% reactcss 1.2.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Case Sandberg - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 reactcss NOTICES AND INFORMATION - -%% readable-stream 2.0.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz) -========================================= -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. - -========================================= -END OF readable-stream NOTICES AND INFORMATION - -%% reflect-metadata 0.1.12 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz) -========================================= -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 -========================================= -END OF reflect-metadata NOTICES AND INFORMATION - -%% regex-not 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016, 2018, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF regex-not NOTICES AND INFORMATION - -%% remark-parse 5.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/remark-parse/-/remark-parse-5.0.0.tgz) -========================================= -(The MIT License) - -Copyright (c) 2014-2016 Titus Wormer -Copyright (c) 2011-2014, Christopher Jeffrey (https://github.com/chjj/) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - -========================================= -END OF remark-parse NOTICES AND INFORMATION - -%% repeat-element 1.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF repeat-element NOTICES AND INFORMATION - -%% repeat-string 1.6.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2016, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF repeat-string NOTICES AND INFORMATION - -%% request 2.87.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/request/-/request-2.87.0.tgz) -========================================= -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 -========================================= -END OF request NOTICES AND INFORMATION - -%% request-progress 3.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz) -========================================= -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. -========================================= -END OF request-progress NOTICES AND INFORMATION - -%% requires-port 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz) -========================================= -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. - - -========================================= -END OF requires-port NOTICES AND INFORMATION - -%% resolve-url 0.2.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2013 Simon Lydell - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF resolve-url NOTICES AND INFORMATION - -%% ret 0.1.15 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/ret/-/ret-0.1.15.tgz) -========================================= -Copyright (C) 2011 by Roly Fentanes - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF ret NOTICES AND INFORMATION - -%% roughjs-es5 0.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/roughjs-es5/-/roughjs-es5-0.1.0.tgz) -========================================= -MIT License - -Copyright (c) 2018 Preet Shihn - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 roughjs-es5 NOTICES AND INFORMATION - -%% rxjs 5.5.9 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/rxjs/-/rxjs-5.5.9.tgz) -========================================= - 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-2017 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. - - -========================================= -END OF rxjs NOTICES AND INFORMATION - -%% safe-buffer 5.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz) -========================================= -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. - -========================================= -END OF safe-buffer NOTICES AND INFORMATION - -%% safe-regex 1.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz) -========================================= -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. - -========================================= -END OF safe-regex NOTICES AND INFORMATION - -%% safer-buffer 2.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz) -========================================= -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. - -========================================= -END OF safer-buffer NOTICES AND INFORMATION - -%% sax 1.2.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/sax/-/sax-1.2.4.tgz) -========================================= -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. - -========================================= -END OF sax NOTICES AND INFORMATION - -%% schedule 0.5.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/schedule/-/schedule-0.5.0.tgz) -========================================= -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. - -========================================= -END OF schedule NOTICES AND INFORMATION - -%% semiotic 1.15.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/semiotic/-/semiotic-1.15.1.tgz) -========================================= -Copyright 2017 Elijah Meeks - -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 semiotic NOTICES AND INFORMATION - -%% semiotic-mark 0.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/semiotic-mark/-/semiotic-mark-0.3.0.tgz) -========================================= -Copyright 2017 Elijah Meeks - -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 semiotic-mark NOTICES AND INFORMATION - -%% semver 5.5.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/semver/-/semver-5.5.0.tgz) -========================================= -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. - -========================================= -END OF semver NOTICES AND INFORMATION - -%% set-value 2.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF set-value NOTICES AND INFORMATION - -%% setImmediate (for RxJS 5.5) NOTICES AND INFORMATION BEGIN HERE (https://github.com/YuzuJS/setImmediate) -========================================= -Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF setImmediate NOTICES AND INFORMATION - -%% sizzle (for lodash 4.17) NOTICES AND INFORMATION BEGIN HERE (https://sizzlejs.com/) -========================================= -Copyright (c) 2009 John Resig - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 sizzle NOTICES AND INFORMATION - -%% snapdragon 0.8.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2016, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF snapdragon NOTICES AND INFORMATION - -%% snapdragon-node 2.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF snapdragon-node NOTICES AND INFORMATION - -%% snapdragon-util 3.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF snapdragon-util NOTICES AND INFORMATION - -%% source-map 0.5.7 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz) -========================================= - -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. - -========================================= -END OF source-map NOTICES AND INFORMATION - -%% source-map-resolve 0.5.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014, 2015, 2016, 2017 Simon Lydell - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF source-map-resolve NOTICES AND INFORMATION - -%% source-map-url 0.4.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014 Simon Lydell - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF source-map-url NOTICES AND INFORMATION - -%% split-string 3.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2017, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF split-string NOTICES AND INFORMATION - -%% sshpk 1.14.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz) -========================================= -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. - -========================================= -END OF sshpk NOTICES AND INFORMATION - -%% state-toggle 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.1.tgz) -========================================= -(The MIT License) - -Copyright (c) 2016 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF state-toggle NOTICES AND INFORMATION - -%% static-extend 0.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF static-extend NOTICES AND INFORMATION - -%% string-hash 1.1.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz) -========================================= -To the extend possible by law, The Dark Sky Company, LLC has [waived all -copyright and related or neighboring rights][cc0] to this library. - -[cc0]: http://creativecommons.org/publicdomain/zero/1.0/ - -========================================= -END OF string-hash NOTICES AND INFORMATION - -%% string_decoder 0.10.31 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz) -========================================= -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. - -========================================= -END OF string_decoder NOTICES AND INFORMATION - -%% style-loader 0.23.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz) -========================================= -Copyright JS Foundation and other contributors - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF style-loader NOTICES AND INFORMATION - -%% styled-jsx 3.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/styled-jsx/-/styled-jsx-3.1.0.tgz) -========================================= -MIT License - -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. - -========================================= -END OF styled-jsx NOTICES AND INFORMATION - -%% stylis-rule-sheet 0.0.10 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz) -========================================= -MIT License - -Copyright (c) 2016 Sultan Tarimo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 stylis-rule-sheet NOTICES AND INFORMATION - -%% sudo-prompt 8.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-8.2.0.tgz) -========================================= -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. - - -========================================= -END OF sudo-prompt NOTICES AND INFORMATION - -%% svg-path-bounding-box 1.0.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/svg-path-bounding-box/-/svg-path-bounding-box-1.0.4.tgz) -========================================= -MIT License - -Copyright (c) 2016 Sultan Tarimo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 svg-path-bounding-box NOTICES AND INFORMATION - -%% svgpath 2.2.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/svgpath/-/svgpath-2.2.1.tgz) -========================================= -(The MIT License) - -Copyright (C) 2013-2015 by Vitaly Puzrin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - -========================================= -END OF svgpath NOTICES AND INFORMATION - -%% symbol-observable 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) Sindre Sorhus (sindresorhus.com) -Copyright (c) Ben Lesh - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF symbol-observable NOTICES AND INFORMATION - -%% throttleit 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://github.com/component/throttle/tree/1.0.0) -========================================= -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 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 throttleit NOTICES AND INFORMATION - -%% tinycolor2 1.4.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz) -========================================= -Copyright (c), Brian Grinstead, http://briangrinstead.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. -========================================= -END OF tinycolor2 NOTICES AND INFORMATION - -%% tinyqueue 1.2.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz) -========================================= -ISC License - -Copyright (c) 2017, Vladimir Agafonkin - -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. - -========================================= -END OF tinyqueue NOTICES AND INFORMATION - -%% tmp 0.0.29 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz) -========================================= -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. - -========================================= -END OF tmp NOTICES AND INFORMATION - -%% to-object-path 0.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2016, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF to-object-path NOTICES AND INFORMATION - -%% to-regex 3.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016-2018, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF to-regex NOTICES AND INFORMATION - -%% to-regex-range 2.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF to-regex-range NOTICES AND INFORMATION - -%% tough-cookie 2.3.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz) -========================================= -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. - -=== - -The following exceptions apply: - -=== - -`public_suffix_list.dat` was obtained from - via -. The license for this file is MPL/2.0. The header of -that file reads as follows: - - // This Source Code Form is subject to the terms of the Mozilla Public - // License, v. 2.0. If a copy of the MPL was not distributed with this - // file, You can obtain one at http://mozilla.org/MPL/2.0/. - -========================================= -END OF tough-cookie NOTICES AND INFORMATION - -%% tree-kill 1.2.0 NOTICES AND INFORMATION BEGIN HERE (https://github.com/pkrumins/node-tree-kill) -========================================= -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. - -========================================= -END OF tree-kill NOTICES AND INFORMATION - -%% trim 0.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/trim/-/trim-0.0.1.tgz) -========================================= -(The MIT License) - -Copyright (c) 2012 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.XXX - -========================================= -END OF trim NOTICES AND INFORMATION - -%% trim-trailing-lines 1.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.1.tgz) -========================================= -(The MIT License) - -Copyright (c) 2015 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF trim-trailing-lines NOTICES AND INFORMATION - -%% trough 1.0.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/trough/-/trough-1.0.3.tgz) -========================================= -(The MIT License) - -Copyright (c) 2016 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF trough NOTICES AND INFORMATION - -%% tunnel-agent 0.6.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz) -========================================= -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 -========================================= -END OF tunnel-agent NOTICES AND INFORMATION - -%% tweetnacl 0.14.5 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz) -========================================= -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 - -========================================= -END OF tweetnacl NOTICES AND INFORMATION - -%% typescript-char 0.0.0 NOTICES AND INFORMATION BEGIN HERE (https://github.com/mason-lang/typescript-char) -========================================= -http://unlicense.org/UNLICENSE -========================================= -END OF typescript-char NOTICES AND INFORMATION - -%% uint64be 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/uint64be/-/uint64be-1.0.1.tgz) -========================================= -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. - -========================================= -END OF uint64be NOTICES AND INFORMATION - -%% ultron 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz) -========================================= -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. - - -========================================= -END OF ultron NOTICES AND INFORMATION - -%% underscore 1.8.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz) -========================================= -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. - -========================================= -END OF underscore NOTICES AND INFORMATION - -%% unherit 1.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/unherit/-/unherit-1.1.1.tgz) -========================================= -(The MIT License) - -Copyright (c) 2015 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF unherit NOTICES AND INFORMATION - -%% unicode 10.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/unicode/-/unicode-10.0.0.tgz) -========================================= -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. - -========================================= -END OF unicode NOTICES AND INFORMATION - -%% unified 6.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/unified/-/unified-6.2.0.tgz) -========================================= -(The MIT License) - -Copyright (c) 2015 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF unified NOTICES AND INFORMATION - -%% union-value 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF union-value NOTICES AND INFORMATION - -%% uniqid 5.0.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/uniqid/-/uniqid-5.0.3.tgz) -========================================= -(The MIT License) - -Copyright (c) 2014 Halász Ádám - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 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 uniqid NOTICES AND INFORMATION - -%% unist-util-is 2.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/unist-util-is/-/unist-util-is-2.1.2.tgz) -========================================= -(The MIT License) - -Copyright (c) 2015 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF unist-util-is NOTICES AND INFORMATION - -%% unist-util-remove-position 1.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.2.tgz) -========================================= -(The MIT License) - -Copyright (c) 2016 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF unist-util-remove-position NOTICES AND INFORMATION - -%% unist-util-stringify-position 1.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz) -========================================= -(The MIT License) - -Copyright (c) 2016 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF unist-util-stringify-position NOTICES AND INFORMATION - -%% unist-util-visit 1.4.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.0.tgz) -========================================= -(The MIT License) - -Copyright (c) 2015 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF unist-util-visit NOTICES AND INFORMATION - -%% unist-util-visit-parents 1.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz) -========================================= -(The MIT License) - -Copyright (c) 2016 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF unist-util-visit-parents NOTICES AND INFORMATION - -%% universalify 0.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz) -========================================= -(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. - -========================================= -END OF universalify NOTICES AND INFORMATION - -%% unset-value 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015, 2017, Jon Schlinkert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF unset-value NOTICES AND INFORMATION - -%% untangle (for ptvsd 4) NOTICES AND INFORMATION BEGIN HERE (https://pypi.org/project/untangle/) -========================================= -# Author: Christian Stefanescu - -# Contributions from: - -Florian Idelberger -Apalala - -// Copyright (c) 2011 - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION 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 untangle NOTICES AND INFORMATION - -%% untildify 3.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/untildify/-/untildify-3.0.2.tgz) -========================================= -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. - -========================================= -END OF untildify NOTICES AND INFORMATION - -%% urix 0.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/urix/-/urix-0.1.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2013 Simon Lydell - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF urix NOTICES AND INFORMATION - -%% url-parse 1.4.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz) -========================================= -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. - - -========================================= -END OF url-parse NOTICES AND INFORMATION - -%% use 3.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/use/-/use-3.1.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015-2017, Jon Schlinkert. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF use NOTICES AND INFORMATION - -%% util-deprecate 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz) -========================================= -(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. - -========================================= -END OF util-deprecate NOTICES AND INFORMATION - -%% uuid 3.3.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz) -========================================= -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. - -========================================= -END OF uuid NOTICES AND INFORMATION - -%% validator 9.4.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/validator/-/validator-9.4.1.tgz) -========================================= -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. - -========================================= -END OF validator NOTICES AND INFORMATION - -%% verror 1.10.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/verror/-/verror-1.10.0.tgz) -========================================= -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 - -========================================= -END OF verror NOTICES AND INFORMATION - -%% vfile 2.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz) -========================================= -(The MIT License) - -Copyright (c) 2015 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF vfile NOTICES AND INFORMATION - -%% vfile-location 2.0.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.3.tgz) -========================================= -(The MIT License) - -Copyright (c) 2016 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF vfile-location NOTICES AND INFORMATION - -%% vfile-message 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vfile-message/-/vfile-message-1.0.1.tgz) -========================================= -(The MIT License) - -Copyright (c) 2017 Titus Wormer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF vfile-message NOTICES AND INFORMATION - -%% viz-annotation 0.0.1-3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/viz-annotation/-/viz-annotation-0.0.1-3.tgz) -========================================= -[Default ISC license] - -Copyright 2018 viz-annotation developers - -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. - - -========================================= -END OF viz-annotation NOTICES AND INFORMATION - -%% vscode-debugadapter 1.28.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.28.0.tgz) -========================================= -Copyright (c) Microsoft Corporation - -All rights reserved. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF vscode-debugadapter NOTICES AND INFORMATION - -%% vscode-debugprotocol 1.28.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.28.0.tgz) -========================================= -Copyright (c) Microsoft Corporation - -All rights reserved. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF vscode-debugprotocol NOTICES AND INFORMATION - -%% vscode-extension-telemetry 0.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.0.tgz) -========================================= -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. -========================================= -END OF vscode-extension-telemetry NOTICES AND INFORMATION - -%% vscode-jsonrpc 3.6.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-3.6.2.tgz) -========================================= -Copyright (c) Microsoft Corporation - -All rights reserved. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF vscode-jsonrpc NOTICES AND INFORMATION - -%% vscode-languageclient 4.4.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-4.4.0.tgz) -========================================= -Copyright (c) Microsoft Corporation - -All rights reserved. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF vscode-languageclient NOTICES AND INFORMATION - -%% vscode-languageserver 4.4.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-4.4.0.tgz) -========================================= -Copyright (c) Microsoft Corporation - -All rights reserved. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF vscode-languageserver NOTICES AND INFORMATION - -%% vscode-languageserver-protocol 3.10.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.10.3.tgz) -========================================= -Copyright (c) Microsoft Corporation - -All rights reserved. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF vscode-languageserver-protocol NOTICES AND INFORMATION - -%% vscode-languageserver-types 3.10.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.10.1.tgz) -========================================= -Copyright (c) Microsoft Corporation - -All rights reserved. - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF vscode-languageserver-types NOTICES AND INFORMATION - -%% vscode-uri 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.1.tgz) -========================================= -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. -========================================= -END OF vscode-uri NOTICES AND INFORMATION - -%% webpack (for lodash 4) NOTICES AND INFORMATION BEGIN HERE (https://webpack.js.org/) -========================================= -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. - -========================================= -END OF webpack NOTICES AND INFORMATION - -%% winreg 1.2.4 NOTICES AND INFORMATION BEGIN HERE (https://github.com/fresc81/node-winreg/tree/v1.2.4) -========================================= -This project is released under [BSD 2-Clause License](http://opensource.org/licenses/BSD-2-Clause). - -Copyright (c) 2016, Paul Bottin All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF winreg NOTICES AND INFORMATION - -%% wrappy 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz) -========================================= -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. - -========================================= -END OF wrappy NOTICES AND INFORMATION - -%% ws 1.1.5 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/ws/-/ws-1.1.5.tgz) -========================================= -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. - - -========================================= -END OF ws NOTICES AND INFORMATION - -%% x-is-string 0.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz) -========================================= -Copyright (c) 2014 Matt-Esch. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 x-is-string NOTICES AND INFORMATION - -%% xml2js 0.4.19 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz) -========================================= -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. - -========================================= -END OF xml2js NOTICES AND INFORMATION - -%% xmlbuilder 9.0.7 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz) -========================================= -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. - -========================================= -END OF xmlbuilder NOTICES AND INFORMATION - -%% xtend 4.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz) -========================================= -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. - -========================================= -END OF xtend NOTICES AND INFORMATION - -%% zone.js 0.7.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/zone.js/-/zone.js-0.7.6.tgz) -========================================= -The MIT License - -Copyright (c) 2016 Google, 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. - -========================================= -END OF zone.js NOTICES AND INFORMATION diff --git a/ThirdPartyNotices-Repository.txt b/ThirdPartyNotices-Repository.txt index 8e7951914857..9e7e822af1bb 100644 --- a/ThirdPartyNotices-Repository.txt +++ b/ThirdPartyNotices-Repository.txt @@ -6,15 +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) -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) - +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 @@ -241,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 @@ -753,3 +736,299 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ========================================= END OF nteract NOTICES, INFORMATION, AND LICENSE + +%% less-plugin-inline-urls NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +========================================= +END OF less-plugin-inline-urls NOTICES, INFORMATION, AND LICENSE + +%% vscode-cpptools NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +vscode-cpptools + +Copyright (c) Microsoft Corporation + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the Software), to deal in the +Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT + +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +========================================= +END OF vscode-cpptools NOTICES, INFORMATION, AND LICENSE + +%% mocha NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= + +(The MIT License) + +Copyright (c) 2011-2020 OpenJS Foundation and contributors, https://openjsf.org + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 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 mocha NOTICES, INFORMATION, AND LICENSE + + +%% get-pip NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= + +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 get-pip NOTICES, INFORMATION, AND LICENSE diff --git a/build/.mocha-multi-reporters.config b/build/.mocha-multi-reporters.config new file mode 100644 index 000000000000..abe46f117f5b --- /dev/null +++ b/build/.mocha-multi-reporters.config @@ -0,0 +1,3 @@ +{ + "reporterEnabled": "./build/ci/scripts/spec_with_pid,mocha-junit-reporter" +} diff --git a/build/.mocha.functional.json b/build/.mocha.functional.json new file mode 100644 index 000000000000..71998902e984 --- /dev/null +++ b/build/.mocha.functional.json @@ -0,0 +1,14 @@ +{ + "spec": "./out/test/**/*.functional.test.js", + "require": [ + "out/test/unittests.js" + ], + "exclude": "out/**/*.jsx", + "reporter": "mocha-multi-reporters", + "reporter-option": "configFile=./build/.mocha-multi-reporters.config", + "ui": "tdd", + "recursive": true, + "colors": true, + "exit": true, + "timeout": 180000 +} diff --git a/build/.mocha.functional.opts b/build/.mocha.functional.opts deleted file mode 100644 index 91d80072f43f..000000000000 --- a/build/.mocha.functional.opts +++ /dev/null @@ -1,7 +0,0 @@ -./out/test/**/*.functional.test.js ---require out/test/unittests.js ---exclude out/**/*.jsx ---ui tdd ---recursive ---colors ---timeout 120000 diff --git a/build/.mocha.functional.perf.json b/build/.mocha.functional.perf.json new file mode 100644 index 000000000000..d67cbb73e8f7 --- /dev/null +++ b/build/.mocha.functional.perf.json @@ -0,0 +1,11 @@ +{ + "spec": "./out/test/**/*.functional.test.js", + "exclude-out": "out/**/*.jsx", + "require": ["out/test/unittests.js"], + "reporter": "spec", + "ui": "tdd", + "recursive": true, + "colors": true, + "exit": true, + "timeout": 180000 +} diff --git a/build/.mocha.perf.config b/build/.mocha.perf.config new file mode 100644 index 000000000000..50ae73444d09 --- /dev/null +++ b/build/.mocha.perf.config @@ -0,0 +1,6 @@ +{ + "reporterEnabled": "spec,xunit", + "xunitReporterOptions": { + "output": "xunit-test-results.xml" + } +} diff --git a/build/.mocha.performance.json b/build/.mocha.performance.json new file mode 100644 index 000000000000..84dc3952cc85 --- /dev/null +++ b/build/.mocha.performance.json @@ -0,0 +1,11 @@ +{ + "spec": "./out/test/**/*.functional.test.js", + "require": ["out/test/unittests.js"], + "reporter": "mocha-multi-reporters", + "reporter-option": "configFile=build/.mocha.perf.config", + "ui": "tdd", + "recursive": true, + "colors": true, + "exit": true, + "timeout": 30000 +} diff --git a/build/.mocha.unittests.js.json b/build/.mocha.unittests.js.json new file mode 100644 index 000000000000..a0bc134c7dc8 --- /dev/null +++ b/build/.mocha.unittests.js.json @@ -0,0 +1,9 @@ +{ + "spec": "./out/test/**/*.unit.test.js", + "require": ["source-map-support/register", "out/test/unittests.js"], + "reporter": "mocha-multi-reporters", + "reporter-option": "configFile=build/.mocha-multi-reporters.config", + "ui": "tdd", + "recursive": true, + "colors": true +} diff --git a/build/.mocha.unittests.json b/build/.mocha.unittests.json new file mode 100644 index 000000000000..cb6bff959497 --- /dev/null +++ b/build/.mocha.unittests.json @@ -0,0 +1,13 @@ +{ + "spec": "./out/test/**/*.unit.test.js", + "require": [ + "out/test/unittests.js" + ], + "exclude": "out/**/*.jsx", + "reporter": "mocha-multi-reporters", + "reporter-option": "configFile=./build/.mocha-multi-reporters.config", + "ui": "tdd", + "recursive": true, + "colors": true, + "timeout": 180000 +} diff --git a/build/.mocha.unittests.opts b/build/.mocha.unittests.opts deleted file mode 100644 index aa90274801e3..000000000000 --- a/build/.mocha.unittests.opts +++ /dev/null @@ -1,5 +0,0 @@ -./out/test/**/*.unit.test.js ---require out/test/unittests.js ---ui tdd ---recursive ---colors diff --git a/build/.mocha.unittests.ts.json b/build/.mocha.unittests.ts.json new file mode 100644 index 000000000000..b20e02bfa96f --- /dev/null +++ b/build/.mocha.unittests.ts.json @@ -0,0 +1,9 @@ +{ + "spec": "./src/test/**/*.unit.test.ts", + "require": ["ts-node/register", "out/test/unittests.js"], + "reporter": "mocha-multi-reporters", + "reporter-option": "configFile=build/.mocha-multi-reporters.config", + "ui": "tdd", + "recursive": true, + "colors": true +} diff --git a/build/.nycrc b/build/.nycrc index 0646aa44fe16..b92a4f36785d 100644 --- a/build/.nycrc +++ b/build/.nycrc @@ -1,20 +1,9 @@ { - "check-coverage": false, - "per-file": true, + "extends": "@istanbuljs/nyc-config-typescript", + "all": true, "include": [ - "out/client/**/*.js" + "src/client/**/*.ts", "out/client/**/*.js" ], - "exclude": [ - "out/test/*.js", - "out/**/*.jsx" - ], - "reporter": [ - "lcov", - "html" - ], - "extension": [ - ".js" - ], - "cache": true, - "all": true + "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..c300039f4ef4 --- /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.10' + 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..024417da0e00 --- /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.10' + 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/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/conda_base.yml b/build/ci/conda_base.yml new file mode 100644 index 000000000000..a1b589e38a32 --- /dev/null +++ b/build/ci/conda_base.yml @@ -0,0 +1 @@ +pip diff --git a/build/ci/conda_env_1.yml b/build/ci/conda_env_1.yml new file mode 100644 index 000000000000..e3230ad0096e --- /dev/null +++ b/build/ci/conda_env_1.yml @@ -0,0 +1,4 @@ +name: conda_env_1 +dependencies: + - python=3.10 + - pip diff --git a/build/ci/conda_env_2.yml b/build/ci/conda_env_2.yml new file mode 100644 index 000000000000..38f551da2580 --- /dev/null +++ b/build/ci/conda_env_2.yml @@ -0,0 +1,4 @@ +name: conda_env_2 +dependencies: + - python=3.10 + - pip diff --git a/build/ci/mocha-vsts-reporter.js b/build/ci/mocha-vsts-reporter.js deleted file mode 100644 index f88a5808076d..000000000000 --- a/build/ci/mocha-vsts-reporter.js +++ /dev/null @@ -1,58 +0,0 @@ -'use-strict'; - -var mocha = require('mocha'); -var MochaJUnitReporter = require('mocha-junit-reporter'); -module.exports = MochaVstsReporter; - -function MochaVstsReporter(runner, options) { - MochaJUnitReporter.call(this, runner, options); - var INDENT_BASE = ' '; - var indenter = ''; - var indentLevel = 0; - var passes = 0; - var failures = 0; - var skipped = 0; - - runner.on('suite', function(suite){ - if (suite.root === true){ - indentLevel++; - indenter = INDENT_BASE.repeat(indentLevel); - } else { - indentLevel++; - indenter = INDENT_BASE.repeat(indentLevel); - } - }); - - runner.on('suite end', function(suite){ - if (suite.root === true) { - indentLevel=0; - indenter = ''; - } else { - indentLevel--; - indenter = INDENT_BASE.repeat(indentLevel); - // ##vso[task.setprogress]current operation - } - }); - - runner.on('pass', function(test){ - passes++; - console.log('%s✓ %s (%dms)', indenter, test.title, test.duration); - }); - - runner.on('pending', function(test){ - skipped++; - console.log('%s- %s', indenter, test.title); - }); - - runner.on('fail', function(test, err){ - failures++; - console.log('%s✖ %s -- error: %s', indenter, test.title, err.message); - console.log('##vso[task.logissue type=error;sourcepath=%s;]FAILED %s :: %s', test.file, test.parent.title, test.title); - }); - - runner.on('end', function(){ - console.log('SUMMARY: %d/%d passed, %d skipped', passes, passes + failures, skipped); - }); -} - -mocha.utils.inherits(MochaVstsReporter, MochaJUnitReporter); 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/compile-and-validate.yml b/build/ci/templates/compile-and-validate.yml deleted file mode 100644 index ea00096fa6d0..000000000000 --- a/build/ci/templates/compile-and-validate.yml +++ /dev/null @@ -1,184 +0,0 @@ -parameters: - name: 'Prebuild' - PythonVersion: '3.7' - NodeVersion: '8.11.2' - NpmVersion: 'latest' - pool: - name: 'Hosted Ubuntu 1604' - MOCHA_CI_REPORTER_ID: '$(Build.SourcesDirectory)/build/ci/mocha-vsts-reporter.js' - MOCHA_CI_REPORTFILE: '$(Build.ArtifactStagingDirectory)/reports/junit-report.xml' - MOCHA_REPORTER_JUNIT: true - RunHygiene: true - UploadBinary: false - AzureStorageAccountName: 'vscodepythonci' - AzureStorageContainerName: 'vscode-python-ci' - -jobs: -- job: ${{ parameters.name }} - pool: ${{ parameters.pool }} - - variables: - nodeVersion: ${{ parameters.NodeVersion }} - npmVersion: ${{ parameters.NpmVersion }} - pythonVersion: ${{ parameters.PythonVersion }} - mochaReportFile: ${{ parameters.MOCHA_CI_REPORTFILE }} - MOCHA_CI_REPORTER_ID: ${{ parameters.MOCHA_CI_REPORTER_ID }} - MOCHA_CI_REPORTFILE: ${{ parameters.MOCHA_CI_REPORTFILE }} - MOCHA_REPORTER_JUNIT: ${{ parameters.MOCHA_REPORTER_JUNIT }} - runHygiene: ${{ parameters.RunHygiene }} - uploadBinary: ${{ parameters.UploadBinary }} - azureStorageAcctName: ${{ parameters.AzureStorageAccountName }} - azureStorageContainerName: ${{ parameters.AzureStorageContainerName }} - plaform: ${{ parameters.Platform }} - - steps: - - bash: | - printenv - - displayName: 'Show all env vars' - condition: variables['system.debug'] - - - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: 'Component Detection' - - continueOnError: true - condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) - - - - task: NodeTool@0 - displayName: 'Use Node $(nodeVersion)' - inputs: - versionSpec: '$(nodeVersion)' - - - - task: UsePythonVersion@0 - displayName: 'Use Python $(pythonVersion)' - inputs: - versionSpec: '$(pythonVersion)' - - - - task: Npm@1 - displayName: 'update npm' - inputs: - command: custom - - verbose: true - - customCommand: 'install -g npm@$(npmVersion)' - - - - task: Npm@1 - displayName: 'npm ci' - inputs: - command: custom - - verbose: true - - customCommand: ci - - - - bash: echo AVAILABLE DEPENDENCY VERSIONS - - echo Node Version = `node -v` - - echo NPM Version = `npm -v` - - echo Python Version = `python --version` - - displayName: 'Show build dependency versions' - name: 'show_bld_deps_vers' - condition: and(succeeded(), variables['system.debug']) - - - - task: Gulp@0 - displayName: 'gulp prePublishNonBundle' - inputs: - targets: 'prePublishNonBundle' - - - - task: Gulp@0 - displayName: 'gulp code hygiene' - inputs: - targets: 'hygiene-modified' - condition: and(succeeded(), eq(variables['runHygiene'], 'true')) - - - - task: Npm@1 - displayName: 'run cover:enable' - inputs: - command: custom - - verbose: false - - customCommand: 'run cover:enable' - - - - task: Npm@1 - displayName: 'run test:unittests' - inputs: - command: custom - - verbose: false - - customCommand: 'run test:unittests:cover' - - - - bash: 'bash <(curl -s https://codecov.io/bash) -t $(COV_UUID)' - displayName: 'publish codecov' - continueOnError: true - condition: always() - - - task: PublishTestResults@2 - displayName: 'Publish JUnit test results' - condition: always() - inputs: - testResultsFiles: '$(MOCHA_CI_REPORTFILE)' - searchFolder: '$(Build.ArtifactStagingDirectory)' - testRunTitle: 'UnitTests $(Platform)-py$(pythonVersion)' - buildPlatform: '$(Platform)-Py$(pythonVersion)' - buildConfiguration: 'Unittests' - - - - task: CmdLine@1 - displayName: 'pip upgrade pip' - inputs: - filename: python - - arguments: '-m pip install --upgrade pip' - - - - task: CmdLine@1 - displayName: 'pip install test requirements' - inputs: - filename: python - - arguments: '-m pip install --upgrade -r ./build/test-requirements.txt' - - - - task: CmdLine@1 - displayName: 'pip install python packages' - inputs: - filename: python - - arguments: '-m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt' - - - - task: ArchiveFiles@2 - displayName: 'Capture Binaries' - inputs: - rootFolderOrFile: out - - archiveFile: '$(Build.ArtifactStagingDirectory)/bin-artifacts.zip' - - - - task: AzureCLI@1 - displayName: 'Upload bin-artifacts to cloud-store' - condition: and(succeeded(), eq(variables['uploadBinary'], 'true')) - inputs: - azureSubscription: 'dekeeler-ptvsazure-acct-for-dev' - scriptLocation: inlineScript - inlineScript: | - az storage container create --account-name $(azureStorageAcctName) --name $(azureStorageContainerName) - - az storage blob upload --account-name $(azureStorageAcctName) --file "$(Build.ArtifactStagingDirectory)/bin-artifacts.zip" --container-name $(azureStorageContainerName) --name "$(Build.BuildNumber)/bin-artifacts.zip" diff --git a/build/ci/templates/test-phase-job.yml b/build/ci/templates/test-phase-job.yml deleted file mode 100644 index 17a91fafd7c2..000000000000 --- a/build/ci/templates/test-phase-job.yml +++ /dev/null @@ -1,48 +0,0 @@ -parameters: - PythonVersion: '3.7' - NodeVersion: '8.11.2' - NpmVersion: 'latest' - PoolName: 'Hosted Ubuntu 1604' - MOCHA_CI_REPORTER_ID: '$(Build.SourcesDirectory)/build/ci/mocha-vsts-reporter.js' - MOCHA_CI_REPORTFILE: '$(Build.ArtifactStagingDirectory)/reports/junit-report.xml' - MOCHA_REPORTER_JUNIT: true - UploadBinary: false - AzureStorageAccountName: 'vscodepythonci' - AzureStorageContainerName: 'vscode-python-ci' - BuildNumber: '$(Build.BuildNumber)' - Platform: 'Linux' - DependsOn: 'Prebuild' - -jobs: -- job: - displayName: ${{ format('SystemTest {0} Py{1}', parameters.Platform, parameters.PythonVersion) }} - dependsOn: ${{ parameters.DependsOn }} - pool: ${{ parameters.pool }} - - variables: - # TODO: use {{ insert }}: {{ parameters.variables }}, it would not work at time I wrote this - nodeVersion: ${{ parameters.NodeVersion }} - npmVersion: ${{ parameters.NpmVersion }} - pythonVersion: ${{ parameters.PythonVersion }} - mochaReportFile: ${{ parameters.MOCHA_CI_REPORTFILE }} - MOCHA_CI_REPORTER_ID: ${{ parameters.MOCHA_CI_REPORTER_ID }} - MOCHA_CI_REPORTFILE: ${{ parameters.MOCHA_CI_REPORTFILE }} - MOCHA_REPORTER_JUNIT: ${{ parameters.MOCHA_REPORTER_JUNIT }} - uploadBinary: ${{ parameters.UploadBinary }} - azureStorageAccountName: ${{ parameters.AzureStorageAccountName }} - azureStorageContainerName: ${{ parameters.AzureStorageContainerName }} - platform: ${{ parameters.Platform }} - buildNumber: ${{ parameters.BuildNumber }} - - strategy: - matrix: - SingleWorkspace: - TestSuiteName: 'testSingleWorkspace' - MultiWorkspace: - TestSuiteName: 'testMultiWorkspace' - Debugger: - TestSuiteName: 'testDebugger' - - - steps: - - template: test-phase.yml diff --git a/build/ci/templates/test-phase.yml b/build/ci/templates/test-phase.yml deleted file mode 100644 index 85cf7d9b5ea5..000000000000 --- a/build/ci/templates/test-phase.yml +++ /dev/null @@ -1,193 +0,0 @@ -# These are the used parameters in this definition: -# TODO: Find a way to make default values become overridden with values -# specified in the master YAML definition... -# TestSuiteName: '' -# BuildNumber: '' -# Platform: '' -# PythonVersion: '3.7' -# NodeVersion: '8.11.2' -# NpmVersion: 'latest' -# MOCHA_CI_REPORTER_ID: '$(Build.SourcesDirectory)/build/ci/mocha-vsts-reporter.js' -# MOCHA_CI_REPORTFILE: '$(Build.ArtifactStagingDirectory)/reports/junit-report.xml' -# MOCHA_REPORTER_JUNIT: true -# AzureStorageAccountName: 'vscodepythonartifacts' -# AzureStorageContainerName: 'pvsc-ci-yaml-artifacts' - -steps: - - bash: | - printenv - - displayName: 'Show all env vars' - condition: variables['system.debug'] - - - powershell: | - New-Item -ItemType directory -Path "$(System.ArtifactsDirectory)/bin-artifacts" - - $buildArtifactUri = "https://$(AzureStorageAccountName).blob.core.windows.net/$(AzureStorageContainerName)/$(Build.BuildNumber)/bin-artifacts.zip" - Write-Verbose "Downloading from $buildArtifactUri" - - $destination = "$(System.ArtifactsDirectory)/bin-artifacts/bin-artifacts.zip" - Write-Verbose "Destination file: $destination" - - Invoke-WebRequest -Uri $buildArtifactUri -OutFile $destination -Verbose - - displayName: 'Download bin-artifacts from cloud-storage' - - - - task: ExtractFiles@1 - displayName: 'Splat bin-artifacts' - inputs: - archiveFilePatterns: '$(System.ArtifactsDirectory)/bin-artifacts/bin-artifacts.zip' - - destinationFolder: '$(Build.SourcesDirectory)' - - cleanDestinationFolder: false - - - - task: NodeTool@0 - displayName: 'Use Node $(NodeVersion)' - inputs: - versionSpec: '$(NodeVersion)' - - - - task: UsePythonVersion@0 - displayName: 'Use Python $(PythonVersion)' - inputs: - versionSpec: '$(PythonVersion)' - - - - bash: echo SHOW ACQUIRED PYTHON - - echo Python Version = `python --version` - - echo Reported Python Path = `python -c "import sys;print(sys.executable)"` - - displayName: 'Show Python Version' - - - - task: PythonScript@0 - displayName: 'Set CI_PYTHON_PATH' - inputs: - scriptSource: inline - failOnStderr: true - script: | - from __future__ import print_function - - import sys - - print('##vso[task.setvariable variable=CI_PYTHON_PATH;]{}'.format(sys.executable)) - - - - task: Npm@1 - displayName: 'update npm' - inputs: - command: custom - - verbose: true - - customCommand: 'install -g npm@$(NpmVersion)' - - - - bash: echo AVAILABLE DEPENDENCY VERSIONS - - echo Node Version = `node -v` - - echo NPM Version = `npm -v` - - echo Python Version = `python --version` - - echo CI_PYTHON_PATH = $CI_PYTHON_PATH - - echo Reported Python Path = `python -c "import sys;print(sys.executable)"` - - displayName: 'Show build dependency versions' - - - - task: Npm@1 - displayName: 'npm ci' - inputs: - command: custom - - verbose: true - - customCommand: ci - - - - task: CmdLine@1 - displayName: 'pip upgrade pip' - inputs: - filename: python - - arguments: '-m pip install --upgrade pip' - - - - task: CmdLine@1 - displayName: 'pip install requirements' - inputs: - filename: python - - arguments: '-m pip install --upgrade -r ./build/test-requirements.txt' - - - - task: CmdLine@1 - displayName: 'pip install ptvsd' - inputs: - filename: python - - arguments: '-m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt' - - - - script: | - set -e - /usr/bin/Xvfb :10 -ac >> /tmp/Xvfb.out 2>&1 & - disown -ar - displayName: 'Start xvfb' - condition: and(succeeded(), eq(variables['Platform'], 'Linux')) - - - - task: Npm@1 - displayName: 'run $(TestSuiteName)' - inputs: - command: custom - - verbose: true - - customCommand: 'run $(TestSuiteName)' - env: - DISPLAY: :10 - - - - task: PythonScript@0 - displayName: 'Ensure test results' - inputs: - scriptSource: inline - failOnStderr: true - script: | - from __future__ import print_function - - import os - import sys - - - test_logfile = os.environ.get('MOCHA_CI_REPORTFILE') - - if not os.path.exists(test_logfile): - print('##vso[task.logissue type=error]Cannot find mocha test results file {}. Did the test run actually fail?'.format(test_logfile)) - print('ERROR: Log file could not be found. Ensure test run did not fail.', file=sys.stderr) - - - - task: PublishTestResults@2 - displayName: 'Publish JUnit test results' - condition: always() - inputs: - testResultsFiles: '$(MOCHA_CI_REPORTFILE)' - searchFolder: '$(Build.ArtifactStagingDirectory)' - testRunTitle: '$(Platform) py$(pythonVersion) $(TestSuiteName)' - buildPlatform: '$(Platform)-py$(pythonVersion)' - buildConfiguration: '$(TestSuiteName)' - - - - bash: 'bash <(curl -s https://codecov.io/bash) -t $(COV_UUID) -F $(Platform)' - displayName: 'publish codecov' - continueOnError: true - condition: always() diff --git a/build/ci/templates/virtual_env_tests.yml b/build/ci/templates/virtual_env_tests.yml deleted file mode 100644 index 60172e2f2d39..000000000000 --- a/build/ci/templates/virtual_env_tests.yml +++ /dev/null @@ -1,222 +0,0 @@ -parameters: - PythonVersion: '3.7' - NodeVersion: '8.11.2' - NpmVersion: 'latest' - AzureStorageAccountName: 'vscodepythonci' - AzureStorageContainerName: 'vscode-python-ci' - Platform: 'Windows' - pool: - name: 'Hosted VS2017' - EnvironmentExecutableFolder: 'Scripts' - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - TEST_FILES_SUFFIX: 'testvirtualenvs' - TestSuiteName: 'testSingleWorkspace' - DependsOn: 'Prebuild' - MOCHA_CI_REPORTER_ID: '$(Build.SourcesDirectory)/build/ci/mocha-vsts-reporter.js' - MOCHA_CI_REPORTFILE: '$(Build.ArtifactStagingDirectory)/reports/junit-report.xml' - MOCHA_REPORTER_JUNIT: true - -jobs: -- job: - displayName: ${{ format('VirtualEnvTest {0} Py{1}', parameters.Platform, parameters.PythonVersion) }} - dependsOn: ${{ parameters.DependsOn }} - pool: ${{ parameters.pool }} - - variables: - nodeVersion: ${{ parameters.NodeVersion }} - npmVersion: ${{ parameters.NpmVersion }} - pythonVersion: ${{ parameters.PythonVersion }} - platform: ${{ parameters.Platform }} - azureStorageAcctName: ${{ parameters.AzureStorageAccountName }} - azureStorageContainerName: ${{ parameters.AzureStorageContainerName }} - environmentExecutableFolder: ${{ parameters.EnvironmentExecutableFolder }} - PYTHON_VIRTUAL_ENVS_LOCATION: ${{ parameters.PYTHON_VIRTUAL_ENVS_LOCATION }} - TEST_FILES_SUFFIX: ${{ parameters.TEST_FILES_SUFFIX }} - TestSuiteName: ${{ parameters.TestSuiteName }} - MOCHA_CI_REPORTER_ID: ${{ parameters.MOCHA_CI_REPORTER_ID }} - MOCHA_CI_REPORTFILE: ${{ parameters.MOCHA_CI_REPORTFILE }} - MOCHA_REPORTER_JUNIT: ${{ parameters.MOCHA_REPORTER_JUNIT }} - - steps: - - bash: | - printenv - - displayName: 'Show all env vars' - condition: variables['system.debug'] - - - - powershell: | - New-Item -ItemType directory -Path "$(System.ArtifactsDirectory)/bin-artifacts" - - $buildArtifactUri = "https://$(azureStorageAcctName).blob.core.windows.net/$(azureStorageContainerName)/$(Build.BuildNumber)/bin-artifacts.zip" - Write-Verbose "Downloading from $buildArtifactUri" - - $destination = "$(System.ArtifactsDirectory)/bin-artifacts/bin-artifacts.zip" - Write-Verbose "Destination file: $destination" - - Invoke-WebRequest -Uri $buildArtifactUri -OutFile $destination -Verbose - - displayName: 'Download bin-artifacts from cloud-storage' - - - - task: ExtractFiles@1 - displayName: 'Splat bin-artifacts' - inputs: - archiveFilePatterns: '$(System.ArtifactsDirectory)/bin-artifacts/bin-artifacts.zip' - - destinationFolder: '$(Build.SourcesDirectory)' - - cleanDestinationFolder: false - - - - task: NodeTool@0 - displayName: 'Use Node $(nodeVersion)' - inputs: - versionSpec: '$(nodeVersion)' - - - - task: UsePythonVersion@0 - displayName: 'Use Python $(pythonVersion)' - inputs: - versionSpec: '$(pythonVersion)' - - - - task: CmdLine@1 - displayName: 'pip install pipenv' - inputs: - filename: python - - arguments: '-m pip install pipenv' - - - - bash: | - pipenv run python ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) pipenvPath - - displayName: 'Create and save pipenv environment' - - - - task: CmdLine@1 - displayName: 'Create venv environment' - inputs: - filename: python - - arguments: '-m venv .venv' - - - - bash: | - .venv/$(environmentExecutableFolder)/python ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) venvPath - - displayName: 'Save venv environment executable' - - - - task: CmdLine@1 - displayName: 'pip install virtualenv' - inputs: - filename: python - - arguments: '-m pip install virtualenv' - - - - task: CmdLine@1 - displayName: 'Create virtualenv environment' - inputs: - filename: python - - arguments: '-m virtualenv .virtualenv' - - - - bash: | - .virtualenv/$(environmentExecutableFolder)/python ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) virtualEnvPath - - displayName: 'Save virtualenv environment executable' - - - powershell: | - Write-Host $Env:CONDA - Write-Host $Env:PYTHON_VIRTUAL_ENVS_LOCATION - - if( '$(platform)' -eq 'Windows' ){ - $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath python - - } else{ - $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath $(environmentExecutableFolder) | Join-Path -ChildPath python - $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath $(environmentExecutableFolder) | Join-Path -ChildPath conda - & $condaPythonPath ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) condaExecPath $condaExecPath - - } - - & $condaPythonPath ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) condaPath - - Get-Content $Env:PYTHON_VIRTUAL_ENVS_LOCATION - - displayName: 'Save conda environment executable' - - - - task: Npm@1 - displayName: 'update npm' - inputs: - command: custom - - verbose: true - - customCommand: 'install -g npm@$(NpmVersion)' - - - - task: Npm@1 - displayName: 'npm ci' - inputs: - command: custom - - verbose: true - - customCommand: ci - - - - script: | - set -e - /usr/bin/Xvfb :10 -ac >> /tmp/Xvfb.out 2>&1 & - disown -ar - displayName: 'Start xvfb' - condition: and(succeeded(), eq(variables['Platform'], 'Linux')) - - - - task: Npm@1 - displayName: 'run $(TestSuiteName)' - inputs: - command: custom - - verbose: true - - customCommand: 'run $(TestSuiteName)' - env: - DISPLAY: :10 - - - task: PythonScript@0 - displayName: 'Ensure test results' - inputs: - scriptSource: inline - failOnStderr: true - script: | - from __future__ import print_function - - import os - import sys - - - test_logfile = os.environ.get('MOCHA_CI_REPORTFILE') - - if not os.path.exists(test_logfile): - print('##vso[task.logissue type=error]Cannot find mocha test results file {}. Did the test run actually fail?'.format(test_logfile)) - print('ERROR: Log file could not be found. Ensure test run did not fail.', file=sys.stderr) - - - - task: PublishTestResults@2 - displayName: 'Publish JUnit test results' - condition: always() - inputs: - testResultsFiles: '$(MOCHA_CI_REPORTFILE)' - searchFolder: '$(Build.ArtifactStagingDirectory)' - testRunTitle: '$(Platform) py$(pythonVersion) TestVirtualEnv' - buildPlatform: '$(Platform)-py$(pythonVersion)' - buildConfiguration: 'TestVirtualEnv' - - diff --git a/build/ci/vscode-python-nightly-ci.yaml b/build/ci/vscode-python-nightly-ci.yaml deleted file mode 100644 index 782f00d780aa..000000000000 --- a/build/ci/vscode-python-nightly-ci.yaml +++ /dev/null @@ -1,154 +0,0 @@ -resources: -- repo: self - clean: true - -# No trigger specified here. This is a nightly build only and -# there isn't a way to specify scheduled builds in YAML at time -# of writing... -trigger: none - -# Only nightly builds. -pr: none - -jobs: - -# Build the extension and run unit tests on it, if successful upload -# the bits to be used in each subsequent job -- template: templates/compile-and-validate.yml - parameters: - name: 'Prebuild' - pool: - name: 'Hosted Ubuntu 1604' - UploadBinary: true - Platform: 'Linux' - PythonVersion: '3.7' - -# Begin test phases: - -## Virtual Env System Tests -- template: templates/virtual_env_tests.yml - parameters: - Platform: 'Windows' - pool: - name: 'Hosted VS2017' - -- template: templates/virtual_env_tests.yml - parameters: - Platform: 'Linux' - EnvironmentExecutableFolder: 'bin' - pool: - name: 'Hosted Ubuntu 1604' - -- template: templates/virtual_env_tests.yml - parameters: - Platform: 'macOS' - EnvironmentExecutableFolder: 'bin' - pool: - name: 'Hosted macOS' - -## System Tests -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted VS2017' - Platform: 'Windows' - PythonVersion: '3.7' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted VS2017' - Platform: 'Windows' - PythonVersion: '3.6' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted VS2017' - Platform: 'Windows' - PythonVersion: '3.5' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted VS2017' - Platform: 'Windows' - PythonVersion: '3.4' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted VS2017' - Platform: 'Windows' - PythonVersion: '2.7' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted Ubuntu 1604' - Platform: 'Linux' - PythonVersion: '3.7' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted Ubuntu 1604' - Platform: 'Linux' - PythonVersion: '3.6' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted Ubuntu 1604' - Platform: 'Linux' - PythonVersion: '3.5' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted Ubuntu 1604' - Platform: 'Linux' - PythonVersion: '3.4' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted Ubuntu 1604' - Platform: 'Linux' - PythonVersion: '2.7' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted macOS' - Platform: 'macOS' - PythonVersion: '3.7' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted macOS' - Platform: 'macOS' - PythonVersion: '3.6' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted macOS' - Platform: 'macOS' - PythonVersion: '3.5' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted macOS' - Platform: 'macOS' - PythonVersion: '3.4' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted macOS' - Platform: 'macOS' - PythonVersion: '2.7' - diff --git a/build/ci/vscode-python-pr-validation.yaml b/build/ci/vscode-python-pr-validation.yaml deleted file mode 100644 index 0f370335842e..000000000000 --- a/build/ci/vscode-python-pr-validation.yaml +++ /dev/null @@ -1,53 +0,0 @@ -resources: -- repo: self - clean: true - -jobs: - -- template: templates/compile-and-validate.yml - parameters: - name: 'Prebuild' - pool: - name: 'Hosted Ubuntu 1604' - UploadBinary: true - Platform: 'Linux' - - -- template: templates/virtual_env_tests.yml - parameters: - Platform: 'Windows' - pool: - name: 'Hosted VS2017' - -- template: templates/virtual_env_tests.yml - parameters: - Platform: 'Linux' - EnvironmentExecutableFolder: 'bin' - pool: - name: 'Hosted Ubuntu 1604' - -- template: templates/virtual_env_tests.yml - parameters: - Platform: 'macOS' - EnvironmentExecutableFolder: 'bin' - pool: - name: 'Hosted macOS' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted VS2017' - Platform: 'Windows' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted Ubuntu 1604' - Platform: 'Linux' - -- template: templates/test-phase-job.yml - parameters: - pool: - name: 'Hosted macOS' - Platform: 'macOS' - diff --git a/build/constants.js b/build/constants.js index e0a56db3e4a3..73815ebea45f 100644 --- a/build/constants.js +++ b/build/constants.js @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); -exports.ExtensionRootDir = path.join(__dirname, '..'); -const jsonFileWithListOfOldFiles = path.join(__dirname, 'existingFiles.json'); -function getListOfExcludedFiles() { - const files = JSON.parse(fs.readFileSync(jsonFileWithListOfOldFiles).toString()); - return files.map(file => path.join(exports.ExtensionRootDir, file.replace(/\//g, path.sep))); -} -exports.filesNotToCheck = getListOfExcludedFiles(); + +const util = require('./util'); + +exports.ExtensionRootDir = util.ExtensionRootDir; +// This is a list of files that existed before MS got the extension. +exports.existingFiles = util.getListOfFiles('existingFiles.json'); +exports.contributedFiles = util.getListOfFiles('contributedFiles.json'); +exports.isWindows = /^win/.test(process.platform); exports.isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; diff --git a/build/constants.ts b/build/constants.ts deleted file mode 100644 index 7866cdedc2e8..000000000000 --- a/build/constants.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as fs from 'fs'; -import * as path from 'path'; - -export const ExtensionRootDir = path.join(__dirname, '..'); - -const jsonFileWithListOfOldFiles = path.join(__dirname, 'existingFiles.json'); -function getListOfExcludedFiles() { - const files = JSON.parse(fs.readFileSync(jsonFileWithListOfOldFiles).toString()) as string[]; - return files.map(file => path.join(ExtensionRootDir, file.replace(/\//g, path.sep))); -} - -export const filesNotToCheck: string[] = getListOfExcludedFiles(); - -export const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; diff --git a/build/contributedFiles.json b/build/contributedFiles.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/build/contributedFiles.json @@ -0,0 +1 @@ +[] diff --git a/build/coverconfig.json b/build/coverconfig.json deleted file mode 100644 index 201980b0fd63..000000000000 --- a/build/coverconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "enabled": false, - "relativeSourcePath": "../client", - "relativeCoverageDir": "../../coverage", - "ignorePatterns": [ - "**/node_modules/**" - ], - "reports": [ - "text-summary", - "json-summary", - "json", - "html", - "lcov", - "lcovonly", - "cobertura" - ], - "verbose": false -} diff --git a/build/existingFiles.json b/build/existingFiles.json index aa915e79acc5..48ab84ff565d 100644 --- a/build/existingFiles.json +++ b/build/existingFiles.json @@ -135,27 +135,6 @@ "src/client/common/variables/sysTypes.ts", "src/client/common/variables/types.ts", "src/client/debugger/constants.ts", - "src/client/debugger/debugAdapter/Common/Contracts.ts", - "src/client/debugger/debugAdapter/Common/debugStreamProvider.ts", - "src/client/debugger/debugAdapter/Common/processServiceFactory.ts", - "src/client/debugger/debugAdapter/Common/protocolLogger.ts", - "src/client/debugger/debugAdapter/Common/protocolParser.ts", - "src/client/debugger/debugAdapter/Common/protocolWriter.ts", - "src/client/debugger/debugAdapter/Common/Utils.ts", - "src/client/debugger/debugAdapter/DebugClients/DebugClient.ts", - "src/client/debugger/debugAdapter/DebugClients/DebugFactory.ts", - "src/client/debugger/debugAdapter/DebugClients/helper.ts", - "src/client/debugger/debugAdapter/DebugClients/launcherProvider.ts", - "src/client/debugger/debugAdapter/DebugClients/LocalDebugClient.ts", - "src/client/debugger/debugAdapter/DebugClients/localDebugClientV2.ts", - "src/client/debugger/debugAdapter/DebugClients/nonDebugClientV2.ts", - "src/client/debugger/debugAdapter/DebugClients/RemoteDebugClient.ts", - "src/client/debugger/debugAdapter/DebugServers/BaseDebugServer.ts", - "src/client/debugger/debugAdapter/DebugServers/LocalDebugServerV2.ts", - "src/client/debugger/debugAdapter/DebugServers/RemoteDebugServerv2.ts", - "src/client/debugger/debugAdapter/main.ts", - "src/client/debugger/debugAdapter/serviceRegistry.ts", - "src/client/debugger/debugAdapter/types.ts", "src/client/debugger/extension/banner.ts", "src/client/debugger/extension/configuration/baseProvider.ts", "src/client/debugger/extension/configuration/configurationProviderUtils.ts", @@ -191,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", @@ -217,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", @@ -229,7 +206,6 @@ "src/client/language/types.ts", "src/client/language/unicode.ts", "src/client/languageServices/jediProxyFactory.ts", - "src/client/languageServices/languageServerSurveyBanner.ts", "src/client/languageServices/proposeLanguageServerBanner.ts", "src/client/linters/bandit.ts", "src/client/linters/baseLinter.ts", @@ -243,7 +219,7 @@ "src/client/linters/linterManager.ts", "src/client/linters/lintingEngine.ts", "src/client/linters/mypy.ts", - "src/client/linters/pep8.ts", + "src/client/linters/pycodestyle.ts", "src/client/linters/prospector.ts", "src/client/linters/pydocstyle.ts", "src/client/linters/pylama.ts", @@ -272,7 +248,6 @@ "src/client/providers/symbolProvider.ts", "src/client/providers/terminalProvider.ts", "src/client/providers/types.ts", - "src/client/providers/updateSparkLibraryProvider.ts", "src/client/refactor/contracts.ts", "src/client/refactor/proxy.ts", "src/client/telemetry/constants.ts", @@ -293,52 +268,52 @@ "src/client/typeFormatters/contracts.ts", "src/client/typeFormatters/dispatcher.ts", "src/client/typeFormatters/onEnterFormatter.ts", - "src/client/unittests/codeLenses/main.ts", - "src/client/unittests/codeLenses/testFiles.ts", - "src/client/unittests/common/argumentsHelper.ts", - "src/client/unittests/common/constants.ts", - "src/client/unittests/common/debugLauncher.ts", - "src/client/unittests/common/managers/baseTestManager.ts", - "src/client/unittests/common/managers/testConfigurationManager.ts", - "src/client/unittests/common/runner.ts", - "src/client/unittests/common/services/configSettingService.ts", - "src/client/unittests/common/services/storageService.ts", - "src/client/unittests/common/services/testManagerService.ts", - "src/client/unittests/common/services/testResultsService.ts", - "src/client/unittests/common/services/workspaceTestManagerService.ts", - "src/client/unittests/common/testUtils.ts", - "src/client/unittests/common/testVisitors/flatteningVisitor.ts", - "src/client/unittests/common/testVisitors/folderGenerationVisitor.ts", - "src/client/unittests/common/testVisitors/resultResetVisitor.ts", - "src/client/unittests/common/types.ts", - "src/client/unittests/common/xUnitParser.ts", - "src/client/unittests/configuration.ts", - "src/client/unittests/configurationFactory.ts", - "src/client/unittests/display/main.ts", - "src/client/unittests/display/picker.ts", - "src/client/unittests/main.ts", - "src/client/unittests/nosetest/main.ts", - "src/client/unittests/nosetest/runner.ts", - "src/client/unittests/nosetest/services/argsService.ts", - "src/client/unittests/nosetest/services/discoveryService.ts", - "src/client/unittests/nosetest/services/parserService.ts", - "src/client/unittests/nosetest/testConfigurationManager.ts", - "src/client/unittests/pytest/main.ts", - "src/client/unittests/pytest/runner.ts", - "src/client/unittests/pytest/services/argsService.ts", - "src/client/unittests/pytest/services/discoveryService.ts", - "src/client/unittests/pytest/services/parserService.ts", - "src/client/unittests/pytest/testConfigurationManager.ts", - "src/client/unittests/serviceRegistry.ts", - "src/client/unittests/types.ts", - "src/client/unittests/unittest/helper.ts", - "src/client/unittests/unittest/main.ts", - "src/client/unittests/unittest/runner.ts", - "src/client/unittests/unittest/services/argsService.ts", - "src/client/unittests/unittest/services/discoveryService.ts", - "src/client/unittests/unittest/services/parserService.ts", - "src/client/unittests/unittest/socketServer.ts", - "src/client/unittests/unittest/testConfigurationManager.ts", + "src/client/testing/codeLenses/main.ts", + "src/client/testing/codeLenses/testFiles.ts", + "src/client/testing/common/argumentsHelper.ts", + "src/client/testing/common/constants.ts", + "src/client/testing/common/debugLauncher.ts", + "src/client/testing/common/managers/baseTestManager.ts", + "src/client/testing/common/managers/testConfigurationManager.ts", + "src/client/testing/common/runner.ts", + "src/client/testing/common/services/configSettingService.ts", + "src/client/testing/common/services/storageService.ts", + "src/client/testing/common/services/testManagerService.ts", + "src/client/testing/common/services/testResultsService.ts", + "src/client/testing/common/services/workspaceTestManagerService.ts", + "src/client/testing/common/testUtils.ts", + "src/client/testing/common/testVisitors/flatteningVisitor.ts", + "src/client/testing/common/testVisitors/folderGenerationVisitor.ts", + "src/client/testing/common/testVisitors/resultResetVisitor.ts", + "src/client/testing/common/types.ts", + "src/client/testing/common/xUnitParser.ts", + "src/client/testing/configuration.ts", + "src/client/testing/configurationFactory.ts", + "src/client/testing/display/main.ts", + "src/client/testing/display/picker.ts", + "src/client/testing/main.ts", + "src/client/testing/nosetest/main.ts", + "src/client/testing/nosetest/runner.ts", + "src/client/testing/nosetest/services/argsService.ts", + "src/client/testing/nosetest/services/discoveryService.ts", + "src/client/testing/nosetest/services/parserService.ts", + "src/client/testing/nosetest/testConfigurationManager.ts", + "src/client/testing/pytest/main.ts", + "src/client/testing/pytest/runner.ts", + "src/client/testing/pytest/services/argsService.ts", + "src/client/testing/pytest/services/discoveryService.ts", + "src/client/testing/pytest/services/parserService.ts", + "src/client/testing/pytest/testConfigurationManager.ts", + "src/client/testing/serviceRegistry.ts", + "src/client/testing/types.ts", + "src/client/testing/unittest/helper.ts", + "src/client/testing/unittest/main.ts", + "src/client/testing/unittest/runner.ts", + "src/client/testing/unittest/services/argsService.ts", + "src/client/testing/unittest/services/discoveryService.ts", + "src/client/testing/unittest/services/parserService.ts", + "src/client/testing/unittest/socketServer.ts", + "src/client/testing/unittest/testConfigurationManager.ts", "src/client/workspaceSymbols/contracts.ts", "src/client/workspaceSymbols/generator.ts", "src/client/workspaceSymbols/main.ts", @@ -404,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", @@ -425,7 +401,6 @@ "src/test/configuration/interpreterSelector.unit.test.ts", "src/test/constants.ts", "src/test/core.ts", - "src/test/debugger/attach.ptvsd.test.ts", "src/test/debugger/capabilities.test.ts", "src/test/debugger/common/constants.ts", "src/test/debugger/common/debugStreamProvider.test.ts", @@ -525,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", @@ -541,39 +516,39 @@ "src/test/testRunner.ts", "src/test/textUtils.ts", "src/test/unittests.ts", - "src/test/unittests/argsService.test.ts", - "src/test/unittests/banners/languageServerSurvey.unit.test.ts", - "src/test/unittests/banners/proposeNewLanguageServerBanner.unit.test.ts", - "src/test/unittests/common/argsHelper.unit.test.ts", - "src/test/unittests/common/debugLauncher.test.ts", - "src/test/unittests/common/managers/testConfigurationManager.unit.test.ts", - "src/test/unittests/common/services/configSettingService.unit.test.ts", - "src/test/unittests/configuration.unit.test.ts", - "src/test/unittests/configurationFactory.unit.test.ts", - "src/test/unittests/debugger.test.ts", - "src/test/unittests/display/main.test.ts", - "src/test/unittests/helper.ts", - "src/test/unittests/mocks.ts", - "src/test/unittests/nosetest/nosetest.argsService.unit.test.ts", - "src/test/unittests/nosetest/nosetest.discovery.unit.test.ts", - "src/test/unittests/nosetest/nosetest.disovery.test.ts", - "src/test/unittests/nosetest/nosetest.run.test.ts", - "src/test/unittests/nosetest/nosetest.test.ts", - "src/test/unittests/pytest/pytest_unittest_parser_data.ts", - "src/test/unittests/pytest/pytest.argsService.unit.test.ts", - "src/test/unittests/pytest/pytest.discovery.test.ts", - "src/test/unittests/pytest/pytest.discovery.unit.test.ts", - "src/test/unittests/pytest/pytest.run.test.ts", - "src/test/unittests/pytest/pytest.test.ts", - "src/test/unittests/pytest/pytest.testparser.unit.test.ts", - "src/test/unittests/rediscover.test.ts", - "src/test/unittests/serviceRegistry.ts", - "src/test/unittests/stoppingDiscoverAndTest.test.ts", - "src/test/unittests/unittest/unittest.argsService.unit.test.ts", - "src/test/unittests/unittest/unittest.discovery.test.ts", - "src/test/unittests/unittest/unittest.discovery.unit.test.ts", - "src/test/unittests/unittest/unittest.run.test.ts", - "src/test/unittests/unittest/unittest.test.ts", + "src/test/testing/argsService.test.ts", + "src/test/testing/banners/languageServerSurvey.unit.test.ts", + "src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts", + "src/test/testing/common/argsHelper.unit.test.ts", + "src/test/testing/common/debugLauncher.test.ts", + "src/test/testing/common/managers/testConfigurationManager.unit.test.ts", + "src/test/testing/common/services/configSettingService.unit.test.ts", + "src/test/testing/configuration.unit.test.ts", + "src/test/testing/configurationFactory.unit.test.ts", + "src/test/testing/debugger.test.ts", + "src/test/testing/display/main.test.ts", + "src/test/testing/helper.ts", + "src/test/testing/mocks.ts", + "src/test/testing/nosetest/nosetest.argsService.unit.test.ts", + "src/test/testing/nosetest/nosetest.discovery.unit.test.ts", + "src/test/testing/nosetest/nosetest.disovery.test.ts", + "src/test/testing/nosetest/nosetest.run.test.ts", + "src/test/testing/nosetest/nosetest.test.ts", + "src/test/testing/pytest/pytest_unittest_parser_data.ts", + "src/test/testing/pytest/pytest.argsService.unit.test.ts", + "src/test/testing/pytest/pytest.discovery.test.ts", + "src/test/testing/pytest/pytest.discovery.unit.test.ts", + "src/test/testing/pytest/pytest.run.test.ts", + "src/test/testing/pytest/pytest.test.ts", + "src/test/testing/pytest/pytest.testparser.unit.test.ts", + "src/test/testing/rediscover.test.ts", + "src/test/testing/serviceRegistry.ts", + "src/test/testing/stoppingDiscoverAndTest.test.ts", + "src/test/testing/unittest/unittest.argsService.unit.test.ts", + "src/test/testing/unittest/unittest.discovery.test.ts", + "src/test/testing/unittest/unittest.discovery.unit.test.ts", + "src/test/testing/unittest/unittest.run.test.ts", + "src/test/testing/unittest/unittest.test.ts", "src/test/vscode-mock.ts", "src/test/workspaceSymbols/common.ts", "src/test/workspaceSymbols/multiroot.test.ts", 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 bc43c970098f..5c3a9e3116ed 100644 --- a/build/functional-test-requirements.txt +++ b/build/functional-test-requirements.txt @@ -1,5 +1,5 @@ -# List of requirements for functional tests -jupyter -numpy -matplotlib -pandas +# List of requirements for functional tests +versioneer +numpy +pytest +pytest-cov 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 d559f79a3361..ff9afdfc8a2e 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -1,16 +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 -pep8 -prospector +pycodestyle pydocstyle -nose -pytest==3.6.3 -rope +prospector +pytest flask +fastapi +uvicorn django +testscenarios +testtools + +# 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/tsconfig.json b/build/tsconfig.json deleted file mode 100644 index cebfbdbeeffc..000000000000 --- a/build/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "outDir": ".", - "lib": [ - "es6" - ], - "allowJs": true, - "checkJs": true, - "sourceMap": false, - "rootDir": ".", - "removeComments": false, - "experimentalDecorators": true, - "noImplicitThis": false, - "noUnusedLocals": true, - "noUnusedParameters": false, - "strict": true - }, - "include": [ - "**/*.ts" - ], - "exclude": [ - "node_modules", - ".vscode-test", - "src" - ] -} diff --git a/build/tslint-rules/baseRuleWalker.js b/build/tslint-rules/baseRuleWalker.js deleted file mode 100644 index 8104718af1e1..000000000000 --- a/build/tslint-rules/baseRuleWalker.js +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const Lint = require("tslint"); -const constants_1 = require("../constants"); -class BaseRuleWalker extends Lint.RuleWalker { - constructor() { - super(...arguments); - this.filesToIgnore = constants_1.filesNotToCheck; - } - sholdIgnoreCcurrentFile(node) { - const sourceFile = node.getSourceFile(); - return sourceFile && sourceFile.fileName && this.filesToIgnore.indexOf(sourceFile.fileName.replace(/\//g, path.sep)) >= 0; - } -} -exports.BaseRuleWalker = BaseRuleWalker; diff --git a/build/tslint-rules/baseRuleWalker.ts b/build/tslint-rules/baseRuleWalker.ts deleted file mode 100644 index e30d0e7c2081..000000000000 --- a/build/tslint-rules/baseRuleWalker.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 * as Lint from 'tslint'; -import * as ts from 'typescript'; -import { filesNotToCheck } from '../constants'; - -export class BaseRuleWalker extends Lint.RuleWalker { - private readonly filesToIgnore = filesNotToCheck; - protected sholdIgnoreCcurrentFile(node: ts.Node) { - const sourceFile = node.getSourceFile(); - return sourceFile && sourceFile.fileName && this.filesToIgnore.indexOf(sourceFile.fileName.replace(/\//g, path.sep)) >= 0; - } -} diff --git a/build/tslint-rules/copyrightAndStrictHeaderRule.js b/build/tslint-rules/copyrightAndStrictHeaderRule.js deleted file mode 100644 index bb1dc6d4dddd..000000000000 --- a/build/tslint-rules/copyrightAndStrictHeaderRule.js +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const os_1 = require("os"); -const Lint = require("tslint"); -const baseRuleWalker_1 = require("./baseRuleWalker"); -const copyrightHeader = [ - '// Copyright (c) Microsoft Corporation. All rights reserved.', - '// Licensed under the MIT License.', - '', - '\'use strict\';' -]; -const copyrightHeaderNoSpace = [ - '// Copyright (c) Microsoft Corporation. All rights reserved.', - '// Licensed under the MIT License.', - '\'use strict\';' -]; -const allowedCopyrightHeaders = [ - copyrightHeader.join('\n'), copyrightHeader.join('\r\n'), - copyrightHeaderNoSpace.join('\n'), copyrightHeaderNoSpace.join('\r\n'), - '\'use strict\';' -]; -const failureMessage = 'Header must contain either \'use strict\' or [copyright] & \'use strict\' in the Python Extension files'; -class NoFileWithoutCopyrightHeader extends baseRuleWalker_1.BaseRuleWalker { - visitSourceFile(sourceFile) { - if (!this.sholdIgnoreCcurrentFile(sourceFile)) { - const sourceFileContents = sourceFile.getFullText(); - if (sourceFileContents) { - this.validateHeader(sourceFile, sourceFileContents); - } - } - super.visitSourceFile(sourceFile); - } - validateHeader(_sourceFile, sourceFileContents) { - for (const allowedHeader of allowedCopyrightHeaders) { - if (sourceFileContents.startsWith(allowedHeader)) { - return; - } - } - const line1 = sourceFileContents.length > 0 ? sourceFileContents.split(/\r\n|\r|\n/)[0] : ''; - const fix = Lint.Replacement.appendText(0, `${copyrightHeader.join(os_1.EOL)}\n\n`); - this.addFailure(this.createFailure(0, line1.length, failureMessage, fix)); - } -} -class Rule extends Lint.Rules.AbstractRule { - apply(sourceFile) { - return this.applyWithWalker(new NoFileWithoutCopyrightHeader(sourceFile, this.getOptions())); - } -} -Rule.FAILURE_STRING = failureMessage; -exports.Rule = Rule; diff --git a/build/tslint-rules/copyrightAndStrictHeaderRule.ts b/build/tslint-rules/copyrightAndStrictHeaderRule.ts deleted file mode 100644 index 91d0037cc933..000000000000 --- a/build/tslint-rules/copyrightAndStrictHeaderRule.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { EOL } from 'os'; -import * as Lint from 'tslint'; -import * as ts from 'typescript'; -import { BaseRuleWalker } from './baseRuleWalker'; - -const copyrightHeader = [ - '// Copyright (c) Microsoft Corporation. All rights reserved.', - '// Licensed under the MIT License.', - '', - '\'use strict\';' -]; -const copyrightHeaderNoSpace = [ - '// Copyright (c) Microsoft Corporation. All rights reserved.', - '// Licensed under the MIT License.', - '\'use strict\';' -]; -const allowedCopyrightHeaders = [ - copyrightHeader.join('\n'), copyrightHeader.join('\r\n'), - copyrightHeaderNoSpace.join('\n'), copyrightHeaderNoSpace.join('\r\n'), - '\'use strict\';' -]; -const failureMessage = 'Header must contain either \'use strict\' or [copyright] & \'use strict\' in the Python Extension files'; - -class NoFileWithoutCopyrightHeader extends BaseRuleWalker { - public visitSourceFile(sourceFile: ts.SourceFile) { - if (!this.sholdIgnoreCcurrentFile(sourceFile)) { - const sourceFileContents = sourceFile.getFullText(); - if (sourceFileContents) { - this.validateHeader(sourceFile, sourceFileContents); - } - } - - super.visitSourceFile(sourceFile); - } - private validateHeader(_sourceFile: ts.SourceFile, sourceFileContents: string) { - for (const allowedHeader of allowedCopyrightHeaders) { - if (sourceFileContents.startsWith(allowedHeader)) { - return; - } - } - - const line1 = sourceFileContents.length > 0 ? sourceFileContents.split(/\r\n|\r|\n/)[0] : ''; - const fix = Lint.Replacement.appendText(0, `${copyrightHeader.join(EOL)}\n\n`); - this.addFailure(this.createFailure(0, line1.length, failureMessage, fix)); - } -} - -export class Rule extends Lint.Rules.AbstractRule { - public static FAILURE_STRING = failureMessage; - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new NoFileWithoutCopyrightHeader(sourceFile, this.getOptions())); - } -} diff --git a/build/tslint-rules/messagesMustBeLocalizedRule.js b/build/tslint-rules/messagesMustBeLocalizedRule.js deleted file mode 100644 index 8e9c7b6a0163..000000000000 --- a/build/tslint-rules/messagesMustBeLocalizedRule.js +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const Lint = require("tslint"); -const ts = require("typescript"); -const baseRuleWalker_1 = require("./baseRuleWalker"); -const methodNames = [ - // From IApplicationShell (vscode.window) - 'showErrorMessage', 'showInformationMessage', - 'showWarningMessage', 'setStatusBarMessage', - // From IOutputChannel (vscode.OutputChannel) - 'appendLine', 'appendLine' -]; -const failureMessage = 'Messages must be localized in the Python Extension (use src/client/common/utils/localize.ts)'; -class NoStringLiteralsInMessages extends baseRuleWalker_1.BaseRuleWalker { - visitCallExpression(node) { - const prop = node.expression; - if (!this.sholdIgnoreCcurrentFile(node) && - ts.isPropertyAccessExpression(node.expression) && - methodNames.indexOf(prop.name.text) >= 0) { - node.arguments - .filter(arg => ts.isStringLiteral(arg) || ts.isTemplateLiteral(arg)) - .forEach(arg => { - this.addFailureAtNode(arg, failureMessage); - }); - } - super.visitCallExpression(node); - } -} -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/tslint-rules/messagesMustBeLocalizedRule.ts b/build/tslint-rules/messagesMustBeLocalizedRule.ts deleted file mode 100644 index 76df78ee9e90..000000000000 --- a/build/tslint-rules/messagesMustBeLocalizedRule.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as Lint from 'tslint'; -import * as ts from 'typescript'; -import { BaseRuleWalker } from './baseRuleWalker'; - -const methodNames = [ - // From IApplicationShell (vscode.window) - 'showErrorMessage', 'showInformationMessage', - 'showWarningMessage', 'setStatusBarMessage', - // From IOutputChannel (vscode.OutputChannel) - 'appendLine', 'appendLine' -]; - -const failureMessage = 'Messages must be localized in the Python Extension (use src/client/common/utils/localize.ts)'; - -class NoStringLiteralsInMessages extends BaseRuleWalker { - protected visitCallExpression(node: ts.CallExpression): void { - const prop = node.expression as ts.PropertyAccessExpression; - if (!this.sholdIgnoreCcurrentFile(node) && - ts.isPropertyAccessExpression(node.expression) && - methodNames.indexOf(prop.name.text) >= 0) { - node.arguments - .filter(arg => ts.isStringLiteral(arg) || ts.isTemplateLiteral(arg)) - .forEach(arg => { - this.addFailureAtNode(arg, failureMessage); - }); - } - super.visitCallExpression(node); - } -} - -export class Rule extends Lint.Rules.AbstractRule { - public static FAILURE_STRING = failureMessage; - public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { - return this.applyWithWalker(new NoStringLiteralsInMessages(sourceFile, this.getOptions())); - } -} diff --git a/build/unlocalizedFiles.json b/build/unlocalizedFiles.json new file mode 100644 index 000000000000..4da3d450af23 --- /dev/null +++ b/build/unlocalizedFiles.json @@ -0,0 +1,26 @@ +[ + "src/client/activation/activationService.ts", + "src/client/common/installer/channelManager.ts", + "src/client/common/installer/moduleInstaller.ts", + "src/client/common/installer/productInstaller.ts", + "src/client/debugger/extension/hooks/childProcessAttachService.ts", + "src/client/formatters/baseFormatter.ts", + "src/client/formatters/blackFormatter.ts", + "src/client/interpreter/configuration/pythonPathUpdaterService.ts", + "src/client/linters/errorHandlers/notInstalled.ts", + "src/client/linters/errorHandlers/standard.ts", + "src/client/linters/linterCommands.ts", + "src/client/linters/prospector.ts", + "src/client/providers/importSortProvider.ts", + "src/client/providers/objectDefinitionProvider.ts", + "src/client/providers/simpleRefactorProvider.ts", + "src/client/pythonEnvironments/discovery/locators/services/pipEnvService.ts", + "src/client/terminals/codeExecution/helper.ts", + "src/client/testing/common/debugLauncher.ts", + "src/client/testing/common/managers/baseTestManager.ts", + "src/client/testing/common/services/discovery.ts", + "src/client/testing/configuration.ts", + "src/client/testing/display/main.ts", + "src/client/testing/main.ts", + "src/client/workspaceSymbols/generator.ts" +] diff --git a/build/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 new file mode 100644 index 000000000000..c54e204ae7d7 --- /dev/null +++ b/build/util.js @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +exports.ExtensionRootDir = path.dirname(__dirname); +function getListOfFiles(filename) { + filename = path.normalize(filename); + if (!path.isAbsolute(filename)) { + filename = path.join(__dirname, filename); + } + const data = fs.readFileSync(filename).toString(); + const files = JSON.parse(data); + return files.map((file) => 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 23d0851fe747..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'; -Object.defineProperty(exports, "__esModule", { value: true }); -const glob = require("glob"); -const path = require("path"); -const webpack_bundle_analyzer_1 = require("webpack-bundle-analyzer"); -const constants_1 = require("../constants"); + +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,31 +20,32 @@ exports.nodeModulesToExternalize = [ 'unicode/category/Mc', 'unicode/category/Nd', 'unicode/category/Pc', - '@jupyterlab/services', - 'azure-storage', - 'request', - 'request-progress', 'source-map-support', - 'file-matcher', - 'diff-match-patch', 'sudo-prompt', 'node-stream-zip', - 'xml2js' + 'xml2js', ]; +exports.nodeModulesToReplacePaths = [...exports.nodeModulesToExternalize]; function getDefaultPlugins(name) { const plugins = []; - if (!constants_1.isCI) { - plugins.push(new webpack_bundle_analyzer_1.BundleAnalyzerPlugin({ - analyzerMode: 'static', - reportFilename: `${name}.analyzer.html` - })); + // Only run the analyzer on a local machine or if required + if (!constants.isCI || process.env.VSC_PYTHON_FORCE_ANALYZER) { + plugins.push( + new webpack_bundle_analyzer.BundleAnalyzerPlugin({ + analyzerMode: 'static', + reportFilename: `${name}.analyzer.html`, + generateStatsFile: true, + statsFilename: `${name}.stats.json`, + openAnalyzer: false, // Open file manually if you want to see it :) + }), + ); } return plugins; } exports.getDefaultPlugins = getDefaultPlugins; function getListOfExistingModulesInOutDir() { - const outDir = path.join(constants_1.ExtensionRootDir, 'out', 'client'); + const outDir = path.join(constants.ExtensionRootDir, 'out', 'client'); const files = glob.sync('**/*.js', { sync: true, cwd: outDir }); - return files.map(filePath => `./${filePath.slice(0, -3)}`); + return files.map((filePath) => `./${filePath.slice(0, -3)}`); } exports.getListOfExistingModulesInOutDir = getListOfExistingModulesInOutDir; diff --git a/build/webpack/common.ts b/build/webpack/common.ts deleted file mode 100644 index c3eface6a51a..000000000000 --- a/build/webpack/common.ts +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as glob from 'glob'; -import * as path from 'path'; -import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; -import { ExtensionRootDir, isCI } from '../constants'; - -export const nodeModulesToExternalize = [ - 'unicode/category/Lu', - 'unicode/category/Ll', - 'unicode/category/Lt', - 'unicode/category/Lo', - 'unicode/category/Lm', - 'unicode/category/Nl', - 'unicode/category/Mn', - 'unicode/category/Mc', - 'unicode/category/Nd', - 'unicode/category/Pc', - '@jupyterlab/services', - 'azure-storage', - 'request', - 'request-progress', - 'source-map-support', - 'file-matcher', - 'diff-match-patch', - 'sudo-prompt', - 'node-stream-zip', - 'xml2js' -]; - -export function getDefaultPlugins(name: 'extension' | 'debugger' | 'dependencies' | 'datascience-ui') { - const plugins = []; - if (!isCI) { - plugins.push( - new BundleAnalyzerPlugin({ - analyzerMode: 'static', - reportFilename: `${name}.analyzer.html` - }) - ); - } - return plugins; -} - -export function getListOfExistingModulesInOutDir() { - const outDir = path.join(ExtensionRootDir, 'out', 'client'); - const files = glob.sync('**/*.js', { sync: true, cwd: outDir }); - return files.map(filePath => `./${filePath.slice(0, -3)}`); -} diff --git a/build/webpack/loaders/externalizeDependencies.js b/build/webpack/loaders/externalizeDependencies.js index 9ae5ec711517..0ada9b0424d8 100644 --- a/build/webpack/loaders/externalizeDependencies.js +++ b/build/webpack/loaders/externalizeDependencies.js @@ -1,21 +1,27 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const common_1 = require("../common"); -function replaceModule(contents, moduleName, quotes) { - const stringToSearch = `${quotes}${moduleName}${quotes}`; - const stringToReplaceWith = `${quotes}./node_modules/${moduleName}${quotes}`; + +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_1.nodeModulesToExternalize.forEach(moduleName => { + common.nodeModulesToReplacePaths.forEach((moduleName) => { if (source.indexOf(moduleName) > 0) { - source = replaceModule(source, moduleName, '"'); - source = replaceModule(source, moduleName, '\''); + source = replaceModule('import\\(', 'import(', source, moduleName, '"'); + source = replaceModule('import\\(', 'import(', source, moduleName, "'"); + source = replaceModule('require\\(', 'require(', source, moduleName, '"'); + source = replaceModule('require\\(', 'require(', source, moduleName, "'"); + source = replaceModule('from ', 'from ', source, moduleName, '"'); + source = replaceModule('from ', 'from ', source, moduleName, "'"); } }); return source; } +// eslint-disable-next-line camelcase exports.default = default_1; diff --git a/build/webpack/loaders/externalizeDependencies.ts b/build/webpack/loaders/externalizeDependencies.ts deleted file mode 100644 index a6771cad5ad3..000000000000 --- a/build/webpack/loaders/externalizeDependencies.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { nodeModulesToExternalize } from '../common'; - -function replaceModule(contents: string, moduleName: string, quotes: '"' | '\''): string { - const stringToSearch = `${quotes}${moduleName}${quotes}`; - const stringToReplaceWith = `${quotes}./node_modules/${moduleName}${quotes}`; - return contents.replace(new RegExp(stringToSearch, 'gm'), stringToReplaceWith); -} -// tslint:disable:no-default-export no-invalid-this -export default function (source: string) { - nodeModulesToExternalize.forEach(moduleName => { - if (source.indexOf(moduleName) > 0) { - source = replaceModule(source, moduleName, '"'); - source = replaceModule(source, moduleName, '\''); - } - }); - return source; -} diff --git a/build/webpack/loaders/fixEvalRequire.js b/build/webpack/loaders/fixEvalRequire.js deleted file mode 100644 index 53ae03e46b84..000000000000 --- a/build/webpack/loaders/fixEvalRequire.js +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -// tslint:disable:no-default-export no-invalid-this -function default_1(source) { - if (source.indexOf('eval') > 0) { - let matches = source.match(/eval\('require'\)\('.*'\)/gm) || []; - matches.forEach(item => { - const moduleName = item.split('\'')[3]; - const stringToReplaceWith = `require('${moduleName}')`; - source = source.replace(item, stringToReplaceWith); - }); - matches = source.match(/eval\("require"\)\(".*"\)/gm) || []; - matches.forEach(item => { - const moduleName = item.split('\'')[3]; - const stringToReplaceWith = `require("${moduleName}")`; - source = source.replace(item, stringToReplaceWith); - }); - } - return source; -} -exports.default = default_1; diff --git a/build/webpack/loaders/fixEvalRequire.ts b/build/webpack/loaders/fixEvalRequire.ts deleted file mode 100644 index 7cdc031d830a..000000000000 --- a/build/webpack/loaders/fixEvalRequire.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-default-export no-invalid-this -export default function (source: string) { - if (source.indexOf('eval') > 0) { - let matches = source.match(/eval\('require'\)\('.*'\)/gm) || []; - matches.forEach(item => { - const moduleName = item.split('\'')[3]; - const stringToReplaceWith = `require('${moduleName}')`; - source = source.replace(item, stringToReplaceWith); - }); - matches = source.match(/eval\("require"\)\(".*"\)/gm) || []; - matches.forEach(item => { - const moduleName = item.split('\'')[3]; - const stringToReplaceWith = `require("${moduleName}")`; - source = source.replace(item, stringToReplaceWith); - }); - } - return source; -} diff --git a/build/webpack/loaders/jsonloader.js b/build/webpack/loaders/jsonloader.js index a5c8927a7d3a..5ec3c7038681 100644 --- a/build/webpack/loaders/jsonloader.js +++ b/build/webpack/loaders/jsonloader.js @@ -1,8 +1,7 @@ // For some reason this has to be in commonjs format -module.exports = function(source) { - - // Just inline the source and fix up defaults so that they don't - // mess up the logic in the setOptions.js file - return `module.exports = ${source}\nmodule.exports.default = false`; -} +module.exports = function (source) { + // Just inline the source and fix up defaults so that they don't + // mess up the logic in the setOptions.js file + return `module.exports = ${source}\nmodule.exports.default = false`; +}; diff --git a/build/webpack/loaders/remarkLoader.js b/build/webpack/loaders/remarkLoader.js index 8dde61d300ea..5ec3c7038681 100644 --- a/build/webpack/loaders/remarkLoader.js +++ b/build/webpack/loaders/remarkLoader.js @@ -1,9 +1,7 @@ // For some reason this has to be in commonjs format -module.exports = function(source) { - +module.exports = function (source) { // Just inline the source and fix up defaults so that they don't // mess up the logic in the setOptions.js file - return `module.exports = ${source}\nmodule.exports.default = false`; - - } + return `module.exports = ${source}\nmodule.exports.default = false`; +}; diff --git a/build/webpack/nativeOrInteractivePicker.html b/build/webpack/nativeOrInteractivePicker.html new file mode 100644 index 000000000000..46d6f0e7eb52 --- /dev/null +++ b/build/webpack/nativeOrInteractivePicker.html @@ -0,0 +1,8 @@ + + + + + Click here to Open Native Editor
+ Click here to Open Interactive Window + + diff --git a/build/webpack/webpack.datascience-ui.config.js b/build/webpack/webpack.datascience-ui.config.js deleted file mode 100644 index 1e1956db220b..000000000000 --- a/build/webpack/webpack.datascience-ui.config.js +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const CopyWebpackPlugin = require("copy-webpack-plugin"); -const HtmlWebpackPlugin = require("html-webpack-plugin"); -const path = require("path"); -const common_1 = require("./common"); -// tslint:disable-next-line:no-var-requires no-require-imports -const FixDefaultImportPlugin = require('webpack-fix-default-import-plugin'); -const configFileName = 'tsconfig.datascience-ui.json'; -const config = { - entry: ['babel-polyfill', './src/datascience-ui/history-react/index.tsx'], - output: { - path: path.join(__dirname, '..', '..', 'out'), - filename: 'datascience-ui/history-react/index_bundle.js', - publicPath: path.join(__dirname, '..', '..') - }, - mode: 'production', - // Use 'eval' for release and `eval-source-map` for development. - // We need to use one where source is embedded, due to webviews (they restrict resources to specific schemes, - // this seems to prevent chrome from downloading the source maps) - devtool: 'eval', - node: { - fs: 'empty' - }, - plugins: [ - ...common_1.getDefaultPlugins('datascience-ui'), - new HtmlWebpackPlugin({ template: 'src/datascience-ui/history-react/index.html', filename: 'datascience-ui/history-react/index.html' }), - new FixDefaultImportPlugin(), - new CopyWebpackPlugin([ - { from: './**/*.png', to: '.' }, - { from: './**/*.svg', to: '.' }, - { from: './**/*.css', to: '.' }, - { from: './**/*theme*.json', to: '.' } - ]) - ], - resolve: { - // Add '.ts' and '.tsx' as resolvable extensions. - extensions: ['.ts', '.tsx', '.js', '.json'] - }, - module: { - rules: [ - // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'. - { - test: /\.tsx?$/, - use: { - loader: 'awesome-typescript-loader', - options: { - configFileName, - reportFiles: [ - 'src/datascience-ui/**/*.{ts,tsx}' - ] - } - } - }, - { - test: /\.css$/, - use: [ - 'style-loader', - 'css-loader' - ] - }, - { - test: /\.js$/, - include: /node_modules.*remark.*default.*js/, - use: [ - { - loader: path.resolve('./build/datascience/remarkLoader.js'), - options: {} - } - ] - }, - { - test: /\.json$/, - type: 'javascript/auto', - include: /node_modules.*remark.*/, - use: [ - { - loader: path.resolve('./build/webpack/loaders/jsonloader.js'), - options: {} - } - ] - } - ] - } -}; -// tslint:disable-next-line:no-default-export -exports.default = config; diff --git a/build/webpack/webpack.datascience-ui.config.ts b/build/webpack/webpack.datascience-ui.config.ts deleted file mode 100644 index d531ae4a6c83..000000000000 --- a/build/webpack/webpack.datascience-ui.config.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as CopyWebpackPlugin from 'copy-webpack-plugin'; -import * as HtmlWebpackPlugin from 'html-webpack-plugin'; -import * as path from 'path'; -import * as webpack from 'webpack'; -import { getDefaultPlugins } from './common'; - -// tslint:disable-next-line:no-var-requires no-require-imports -const FixDefaultImportPlugin = require('webpack-fix-default-import-plugin'); - -const configFileName = 'tsconfig.datascience-ui.json'; - -const config: webpack.Configuration = { - entry: ['babel-polyfill', './src/datascience-ui/history-react/index.tsx'], - output: { - path: path.join(__dirname, '..', '..', 'out'), - filename: 'datascience-ui/history-react/index_bundle.js', - publicPath: path.join(__dirname, '..', '..') - }, - mode: 'production', // Leave as is, we'll need to see stack traces when there are errors. - // Use 'eval' for release and `eval-source-map` for development. - // We need to use one where source is embedded, due to webviews (they restrict resources to specific schemes, - // this seems to prevent chrome from downloading the source maps) - devtool: 'eval', - node: { - fs: 'empty' - }, - plugins: [ - ...getDefaultPlugins('datascience-ui'), - new HtmlWebpackPlugin({ template: 'src/datascience-ui/history-react/index.html', filename: 'datascience-ui/history-react/index.html' }), - new FixDefaultImportPlugin(), - new CopyWebpackPlugin([ - { from: './**/*.png', to: '.' }, - { from: './**/*.svg', to: '.' }, - { from: './**/*.css', to: '.' }, - { from: './**/*theme*.json', to: '.' } - ]) - ], - resolve: { - // Add '.ts' and '.tsx' as resolvable extensions. - extensions: ['.ts', '.tsx', '.js', '.json'] - }, - - module: { - rules: [ - // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'. - { - test: /\.tsx?$/, - use: { - loader: 'awesome-typescript-loader', - options: { - configFileName, - reportFiles: [ - 'src/datascience-ui/**/*.{ts,tsx}' - ] - } - } - }, - { - test: /\.css$/, - use: [ - 'style-loader', - 'css-loader' - ] - }, - { - test: /\.js$/, - include: /node_modules.*remark.*default.*js/, - use: [ - { - loader: path.resolve('./build/datascience/remarkLoader.js'), - options: {} - } - ] - }, - { - test: /\.json$/, - type: 'javascript/auto', - include: /node_modules.*remark.*/, - use: [ - { - loader: path.resolve('./build/webpack/loaders/jsonloader.js'), - options: {} - } - ] - } - ] - } -}; - -// tslint:disable-next-line:no-default-export -export default config; diff --git a/build/webpack/webpack.debugadapter.config.js b/build/webpack/webpack.debugadapter.config.js deleted file mode 100644 index 82e48252ee5e..000000000000 --- a/build/webpack/webpack.debugadapter.config.js +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const tsconfig_paths_webpack_plugin_1 = require("tsconfig-paths-webpack-plugin"); -const webpack_1 = require("webpack"); -const constants_1 = require("../constants"); -const common_1 = require("./common"); -// tslint:disable-next-line:no-var-requires no-require-imports -const configFileName = path.join(constants_1.ExtensionRootDir, 'tsconfig.extension.json'); -const config = { - mode: 'production', - target: 'node', - entry: { - 'debugger/debugAdapter/main': './src/client/debugger/debugAdapter/main.ts' - }, - devtool: 'source-map', - node: { - __dirname: false - }, - module: { - rules: [ - { - // JupyterServices imports node-fetch using `eval`. - test: /@jupyterlab[\\\/]services[\\\/].*js$/, - use: [ - { - loader: path.join(__dirname, 'loaders', 'fixEvalRequire.js') - } - ] - }, - { - test: /\.ts$/, - exclude: /node_modules/, - use: [ - { - loader: 'ts-loader' - } - ] - } - ] - }, - externals: [ - 'vscode', - 'commonjs' - ], - plugins: [ - ...common_1.getDefaultPlugins('extension') - ], - resolve: { - extensions: ['.ts', '.js'], - plugins: [ - new tsconfig_paths_webpack_plugin_1.TsconfigPathsPlugin({ configFile: configFileName }) - ] - }, - output: { - filename: '[name].js', - path: path.resolve(constants_1.ExtensionRootDir, 'out', 'client'), - libraryTarget: 'commonjs2', - devtoolModuleFilenameTemplate: '../../[resource-path]' - } -}; -// tslint:disable-next-line:no-default-export -exports.default = config; diff --git a/build/webpack/webpack.debugadapter.config.ts b/build/webpack/webpack.debugadapter.config.ts deleted file mode 100644 index 7f5b4f0ee728..000000000000 --- a/build/webpack/webpack.debugadapter.config.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 { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; -import { Configuration, ContextReplacementPlugin } from 'webpack'; -import { ExtensionRootDir } from '../constants'; -import { getDefaultPlugins } from './common'; - -// tslint:disable-next-line:no-var-requires no-require-imports -const configFileName = path.join(ExtensionRootDir, 'tsconfig.extension.json'); - -const config: Configuration = { - mode: 'production', - target: 'node', - entry: { - 'debugger/debugAdapter/main': './src/client/debugger/debugAdapter/main.ts' - }, - devtool: 'source-map', - node: { - __dirname: false - }, - module: { - rules: [ - { - // JupyterServices imports node-fetch using `eval`. - test: /@jupyterlab[\\\/]services[\\\/].*js$/, - use: [ - { - loader: path.join(__dirname, 'loaders', 'fixEvalRequire.js') - } - ] - }, - { - test: /\.ts$/, - exclude: /node_modules/, - use: [ - { - loader: 'ts-loader' - } - ] - } - ] - }, - externals: [ - 'vscode', - 'commonjs' - ], - plugins: [ - ...getDefaultPlugins('extension') - ], - resolve: { - extensions: ['.ts', '.js'], - plugins: [ - new TsconfigPathsPlugin({ configFile: configFileName }) - ] - }, - output: { - filename: '[name].js', - path: path.resolve(ExtensionRootDir, 'out', 'client'), - libraryTarget: 'commonjs2', - devtoolModuleFilenameTemplate: '../../[resource-path]' - } -}; - -// tslint:disable-next-line:no-default-export -export default config; 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 f30cb43bd08e..082ce52a4d32 100644 --- a/build/webpack/webpack.extension.config.js +++ b/build/webpack/webpack.extension.config.js @@ -1,85 +1,90 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const tsconfig_paths_webpack_plugin_1 = require("tsconfig-paths-webpack-plugin"); -const webpack_1 = require("webpack"); -const constants_1 = require("../constants"); -const common_1 = require("./common"); -// tslint:disable-next-line:no-var-requires no-require-imports -const WrapperPlugin = require('wrapper-webpack-plugin'); -// tslint:disable-next-line:no-var-requires no-require-imports -const configFileName = path.join(constants_1.ExtensionRootDir, 'tsconfig.extension.json'); -// Some modules will be pre-genearted and stored in out/.. dir and they'll be referenced via NormalModuleReplacementPlugin -// We need to ensure they do not get bundled into the output (as they are large). -const existingModulesInOutDir = common_1.getListOfExistingModulesInOutDir(); + +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'); + +const configFileName = path.join(constants.ExtensionRootDir, 'tsconfig.extension.json'); +// Some modules will be pre-genearted and stored in out/.. dir and they'll be referenced via +// NormalModuleReplacementPlugin. We need to ensure they do not get bundled into the output +// (as they are large). +const existingModulesInOutDir = common.getListOfExistingModulesInOutDir(); const config = { mode: 'production', target: 'node', entry: { - extension: './src/client/extension.ts' + 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 using `eval`. - test: /@jupyterlab[\\\/]services[\\\/].*js$/, + test: /\.ts$/, use: [ { - loader: path.join(__dirname, 'loaders', 'fixEvalRequire.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', + }, + ], + }, + { + test: /\.worker\.js$/, + use: { loader: 'worker-loader' }, + }, + ], }, externals: [ 'vscode', 'commonjs', - ...existingModulesInOutDir - ], - plugins: [ - ...common_1.getDefaultPlugins('extension'), - new WrapperPlugin({ - test: /\extension.js$/, - // Import source map warning file only if source map is enabled. - // Minimize importing external files. - header: '(function(){if (require(\'vscode\').workspace.getConfiguration(\'python.diagnostics\', undefined).get(\'sourceMapsEnabled\', false)) {require(\'./sourceMapSupport\').default(require(\'vscode\'));}})();' - }) + ...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: { extensions: ['.ts', '.js'], - plugins: [ - new tsconfig_paths_webpack_plugin_1.TsconfigPathsPlugin({ configFile: configFileName }) - ] + plugins: [new tsconfig_paths_webpack_plugin.TsconfigPathsPlugin({ configFile: configFileName })], + conditionNames: ['import', 'require', 'node'], }, output: { filename: '[name].js', - path: path.resolve(constants_1.ExtensionRootDir, 'out', 'client'), + 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.config.ts b/build/webpack/webpack.extension.config.ts deleted file mode 100644 index 7d84edd037a9..000000000000 --- a/build/webpack/webpack.extension.config.ts +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; -import { Configuration, ContextReplacementPlugin } from 'webpack'; -import { ExtensionRootDir } from '../constants'; -import { getDefaultPlugins, getListOfExistingModulesInOutDir } from './common'; - -// tslint:disable-next-line:no-var-requires no-require-imports -const WrapperPlugin = require('wrapper-webpack-plugin'); - -// tslint:disable-next-line:no-var-requires no-require-imports -const configFileName = path.join(ExtensionRootDir, 'tsconfig.extension.json'); - -// Some modules will be pre-genearted and stored in out/.. dir and they'll be referenced via NormalModuleReplacementPlugin -// We need to ensure they do not get bundled into the output (as they are large). -const existingModulesInOutDir = getListOfExistingModulesInOutDir(); - -const config: Configuration = { - mode: 'production', - target: 'node', - entry: { - extension: './src/client/extension.ts' - }, - devtool: 'source-map', - node: { - __dirname: false - }, - module: { - rules: [ - { - // JupyterServices imports node-fetch using `eval`. - test: /@jupyterlab[\\\/]services[\\\/].*js$/, - use: [ - { - loader: path.join(__dirname, 'loaders', 'fixEvalRequire.js') - } - ] - }, - { - test: /\.ts$/, - use: [ - { - loader: path.join(__dirname, 'loaders', 'externalizeDependencies.js') - } - ] - }, - { - test: /\.ts$/, - exclude: /node_modules/, - use: [ - { - loader: 'ts-loader' - } - ] - } - ] - }, - externals: [ - 'vscode', - 'commonjs', - ...existingModulesInOutDir - ], - plugins: [ - ...getDefaultPlugins('extension'), - new WrapperPlugin({ - test: /\extension.js$/, - // Import source map warning file only if source map is enabled. - // Minimize importing external files. - header: '(function(){if (require(\'vscode\').workspace.getConfiguration(\'python.diagnostics\', undefined).get(\'sourceMapsEnabled\', false)) {require(\'./sourceMapSupport\').default(require(\'vscode\'));}})();' - }) - ], - resolve: { - extensions: ['.ts', '.js'], - plugins: [ - new TsconfigPathsPlugin({ configFile: configFileName }) - ] - }, - output: { - filename: '[name].js', - path: path.resolve(ExtensionRootDir, 'out', 'client'), - libraryTarget: 'commonjs2', - devtoolModuleFilenameTemplate: '../../[resource-path]' - } -}; - -// tslint:disable-next-line:no-default-export -export default config; diff --git a/build/webpack/webpack.extension.dependencies.config.js b/build/webpack/webpack.extension.dependencies.config.js index 4eb814957edd..a90e9135a605 100644 --- a/build/webpack/webpack.extension.dependencies.config.js +++ b/build/webpack/webpack.extension.dependencies.config.js @@ -1,51 +1,44 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const constants_1 = require("../constants"); -const common_1 = require("./common"); + +const copyWebpackPlugin = require('copy-webpack-plugin'); +const path = require('path'); +const constants = require('../constants'); +const common = require('./common'); + const entryItems = {}; -common_1.nodeModulesToExternalize.forEach(moduleName => { +common.nodeModulesToExternalize.forEach((moduleName) => { entryItems[`node_modules/${moduleName}`] = `./node_modules/${moduleName}`; }); const config = { mode: 'production', target: 'node', + context: constants.ExtensionRootDir, entry: entryItems, devtool: 'source-map', node: { - __dirname: false + __dirname: false, }, - module: { - rules: [ - { - // JupyterServices imports node-fetch using `eval`. - test: /@jupyterlab[\\\/]services[\\\/].*js$/, - use: [ - { - loader: path.join(__dirname, 'loaders', 'fixEvalRequire.js') - } - ] - } - ] - }, - externals: [ - 'vscode', - 'commonjs' - ], + module: {}, + externals: ['vscode', 'commonjs'], plugins: [ - ...common_1.getDefaultPlugins('dependencies') + ...common.getDefaultPlugins('dependencies'), + // vsls requires our package.json to be next to node_modules. It's how they + // 'find' the calling extension. + // eslint-disable-next-line new-cap + new copyWebpackPlugin({ patterns: [{ from: './package.json', to: '.' }] }), ], resolve: { - extensions: ['.js'] + extensions: ['.js'], }, output: { filename: '[name].js', - path: path.resolve(constants_1.ExtensionRootDir, 'out', 'client'), + 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.ts b/build/webpack/webpack.extension.dependencies.config.ts deleted file mode 100644 index 4f7d3e9c9148..000000000000 --- a/build/webpack/webpack.extension.dependencies.config.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as webpack from 'webpack'; -import { ExtensionRootDir } from '../constants'; -import { getDefaultPlugins, nodeModulesToExternalize } from './common'; - -const entryItems: { [key: string]: string } = {}; -nodeModulesToExternalize.forEach(moduleName => { - entryItems[`node_modules/${moduleName}`] = `./node_modules/${moduleName}`; -}); - -const config: webpack.Configuration = { - mode: 'production', - target: 'node', - entry: entryItems, - devtool: 'source-map', - node: { - __dirname: false - }, - module: { - rules: [ - { - // JupyterServices imports node-fetch using `eval`. - test: /@jupyterlab[\\\/]services[\\\/].*js$/, - use: [ - { - loader: path.join(__dirname, 'loaders', 'fixEvalRequire.js') - } - ] - } - ] - }, - externals: [ - 'vscode', - 'commonjs' - ], - plugins: [ - ...getDefaultPlugins('dependencies') - ], - resolve: { - extensions: ['.js'] - }, - output: { - filename: '[name].js', - path: path.resolve(ExtensionRootDir, 'out', 'client'), - libraryTarget: 'commonjs2', - devtoolModuleFilenameTemplate: '../../[resource-path]' - } -}; - -// tslint:disable-next-line:no-default-export -export default config; diff --git a/build/webpack/webpack.extension.sourceMaps.config.js b/build/webpack/webpack.extension.sourceMaps.config.js deleted file mode 100644 index 465aa24c12df..000000000000 --- a/build/webpack/webpack.extension.sourceMaps.config.js +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const tsconfig_paths_webpack_plugin_1 = require("tsconfig-paths-webpack-plugin"); -const constants_1 = require("../constants"); -const common_1 = require("./common"); -// tslint:disable-next-line:no-var-requires no-require-imports -const configFileName = path.join(constants_1.ExtensionRootDir, 'tsconfig.extension.json'); -// Some modules will be pre-genearted and stored in out/.. dir and they'll be referenced via NormalModuleReplacementPlugin -// We need to ensure they do not get bundled into the output (as they are large). -const existingModulesInOutDir = common_1.getListOfExistingModulesInOutDir(); -const config = { - mode: 'production', - target: 'node', - entry: { - sourceMapSupport: './src/client/sourceMapSupport.ts' - }, - devtool: 'source-map', - node: { - __dirname: false - }, - module: { - rules: [ - { - test: /\.ts$/, - exclude: /node_modules/, - use: [ - { - loader: 'ts-loader' - } - ] - } - ] - }, - externals: [ - 'vscode', - 'commonjs', - ...existingModulesInOutDir - ], - plugins: [ - ...common_1.getDefaultPlugins('dependencies') - ], - resolve: { - extensions: ['.ts', '.js'], - plugins: [ - new tsconfig_paths_webpack_plugin_1.TsconfigPathsPlugin({ configFile: configFileName }) - ] - }, - output: { - filename: '[name].js', - path: path.resolve(constants_1.ExtensionRootDir, 'out', 'client'), - libraryTarget: 'commonjs2', - devtoolModuleFilenameTemplate: '../../[resource-path]' - } -}; -// tslint:disable-next-line:no-default-export -exports.default = config; diff --git a/build/webpack/webpack.extension.sourceMaps.config.ts b/build/webpack/webpack.extension.sourceMaps.config.ts deleted file mode 100644 index 59c571c5466a..000000000000 --- a/build/webpack/webpack.extension.sourceMaps.config.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; -import * as webpack from 'webpack'; -import { ExtensionRootDir } from '../constants'; -import { getDefaultPlugins, getListOfExistingModulesInOutDir } from './common'; - -// tslint:disable-next-line:no-var-requires no-require-imports -const configFileName = path.join(ExtensionRootDir, 'tsconfig.extension.json'); - -// Some modules will be pre-genearted and stored in out/.. dir and they'll be referenced via NormalModuleReplacementPlugin -// We need to ensure they do not get bundled into the output (as they are large). -const existingModulesInOutDir = getListOfExistingModulesInOutDir(); - -const config: webpack.Configuration = { - mode: 'production', - target: 'node', - entry: { - sourceMapSupport: './src/client/sourceMapSupport.ts' - }, - devtool: 'source-map', - node: { - __dirname: false - }, - module: { - rules: [ - { - test: /\.ts$/, - exclude: /node_modules/, - use: [ - { - loader: 'ts-loader' - } - ] - } - ] - }, - externals: [ - 'vscode', - 'commonjs', - ...existingModulesInOutDir - ], - plugins: [ - ...getDefaultPlugins('dependencies') - ], - resolve: { - extensions: ['.ts', '.js'], - plugins: [ - new TsconfigPathsPlugin({ configFile: configFileName }) - ] - }, - output: { - filename: '[name].js', - path: path.resolve(ExtensionRootDir, 'out', 'client'), - libraryTarget: 'commonjs2', - devtoolModuleFilenameTemplate: '../../[resource-path]' - } -}; - -// tslint:disable-next-line:no-default-export -export 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/coverconfig.json b/coverconfig.json deleted file mode 100644 index 273e6a1ce294..000000000000 --- a/coverconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "enabled": false, - "relativeSourcePath": "../client", - "relativeCoverageDir": "../../coverage", - "ignorePatterns": [ - "**/node_modules/**" - ], - "reports": [ - "text-summary", - "json-summary", - "json", - "html", - "lcov", - "lcovonly", - "cobertura" - ], - "verbose": false -} 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/gulpfile.js b/gulpfile.js index 1f204727aa4a..0b919f16572a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,735 +1,289 @@ -/*--------------------------------------------------------------------------------------------- -* Copyright (c) Microsoft Corporation. All rights reserved. -* Licensed under the MIT License. See License.txt in the project root for license information. -*--------------------------------------------------------------------------------------------*/ - -/* jshint node: true */ -/* jshint esversion: 6 */ - -'use strict'; - -const gulp = require('gulp'); -const filter = require('gulp-filter'); -const es = require('event-stream'); -const tsfmt = require('typescript-formatter'); -const tslint = require('tslint'); -const relative = require('relative'); -const ts = require('gulp-typescript'); -const cp = require('child_process'); -const spawn = require('cross-spawn'); -const colors = require('colors/safe'); -const path = require('path'); -const jeditor = require("gulp-json-editor"); -const del = require('del'); -const sourcemaps = require('gulp-sourcemaps'); -const fs = require('fs-extra'); -const fsExtra = require('fs-extra'); -const remapIstanbul = require('remap-istanbul'); -const istanbul = require('istanbul'); -const glob = require('glob'); -const _ = require('lodash'); -const nativeDependencyChecker = require('node-has-native-dependencies'); -const flat = require('flat'); -const inlinesource = require('gulp-inline-source'); - -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/**/*', - 'src/client/**/*', -]; - -const tsFilter = [ - 'src/**/*.ts*', - '!out/**/*' -]; - -const indentationFilter = [ - 'src/**/*.ts*', - '!**/typings/**/*', -]; - -const tslintFilter = [ - 'src/**/*.ts*', - 'test/**/*.ts*', - '!**/node_modules/**', - '!out/**/*', - '!images/**/*', - '!.vscode/**/*', - '!pythonFiles/**/*', - '!resources/**/*', - '!snippets/**/*', - '!syntaxes/**/*', - '!**/typings/**/*', - '!**/*.d.ts' -]; - -gulp.task('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: 'all', skipFormatCheck: true, skipIndentationCheck: true }, done)); - -gulp.task('compile', (done) => run({ mode: 'compile', skipFormatCheck: true, skipIndentationCheck: true, skipLinter: 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('debugger-coverage', buildDebugAdapterCoverage); - -gulp.task('hygiene-watch-branch', () => gulp.watch(tsFilter, gulp.series('hygiene-branch'))); - -gulp.task('hygiene-all', (done) => run({ mode: 'all' }, done)); - -gulp.task('hygiene-branch', (done) => run({ mode: 'diffMaster' }, done)); - -gulp.task('cover:clean', () => del(['coverage', 'debug_coverage*'])); - -gulp.task('output:clean', () => del(['coverage', 'debug_coverage*'])); - -gulp.task('clean:cleanExceptTests', () => del(['clean:vsix', 'out/client', 'out/datascience-ui', 'out/server'])); -gulp.task('clean:vsix', () => del(['*.vsix'])); -gulp.task('clean:out', () => del(['out'])); - -gulp.task('clean', gulp.parallel('output:clean', 'cover:clean', 'clean:vsix', 'clean:out')); - -gulp.task('checkNativeDependencies', (done) => { - if (hasNativeDependencies()) { - throw new Error('Native dependencies deteced'); - } - done(); -}); - -gulp.task('cover:enable', () => { - return gulp.src("./build/coverconfig.json") - .pipe(jeditor((json) => { - json.enabled = true; - return json; - })) - .pipe(gulp.dest("./out", { 'overwrite': true })); -}); - -gulp.task('cover:disable', () => { - return gulp.src("./build/coverconfig.json") - .pipe(jeditor((json) => { - json.enabled = false; - return json; - })) - .pipe(gulp.dest("./out", { 'overwrite': true })); -}); - -/** - * Inline CSS into the coverage report for better visualizations on - * the VSTS report page for code coverage. - */ -gulp.task('inlinesource', () => { - return gulp.src('./coverage/lcov-report/*.html') - .pipe(inlinesource({ attribute: false })) - .pipe(gulp.dest('./coverage/lcov-report-inline')); -}); - -gulp.task('check-datascience-dependencies', () => checkDatascienceDependencies()); - - -gulp.task("compile", () => { - const tsProject = ts.createProject("tsconfig.json"); - return tsProject.src() - .pipe(tsProject()) - .js.pipe(gulp.dest("out")); -}); - - -gulp.task('compile-webviews', async () => spawnAsync('npx', ['webpack', '--config', 'webpack.datascience-ui.config.js', '--mode', 'production'])); -gulp.task('webpack', async () => { - await spawnAsync('npx', ['webpack', '--mode', 'production', '--inline', '--progress']); - await spawnAsync('npx', ['webpack', '--config', './build/webpack/webpack.extension.config.js', '--mode', 'production', '--inline', '--progress']); -}); - -gulp.task('webpack', async () => { - await spawnAsync('npx', ['webpack', '--mode', 'production']); - await spawnAsync('npx', ['webpack', '--config', './build/webpack/webpack.extension.sourceMaps.config.js', '--mode', 'production']); - await spawnAsync('npx', ['webpack', '--config', './build/webpack/webpack.extension.config.js', '--mode', 'production']); - await spawnAsync('npx', ['webpack', '--config', './build/webpack/webpack.debugadapter.config.js', '--mode', 'production']); -}); - -gulp.task('prePublishBundle', gulp.series('checkNativeDependencies', 'check-datascience-dependencies', 'compile', 'clean:cleanExceptTests', 'webpack')); -gulp.task('prePublishNonBundle', gulp.series('checkNativeDependencies', 'check-datascience-dependencies', 'compile', 'compile-webviews')); - -const installPythonLibArgs = ['-m', 'pip', '--disable-pip-version-check', 'install', - '-t', './pythonFiles/lib/python', '--no-cache-dir', '--implementation', 'py', '--no-deps', - '--upgrade', '-r', 'requirements.txt']; -gulp.task('installPythonLibs', async () => { - const requirements = fs.readFileSync(path.join(__dirname, 'requirements.txt'), 'utf8').split('\n').map(item => item.trim()).filter(item => item.length > 0); - const args = ['-m', 'pip', '--disable-pip-version-check', 'install', '-t', './pythonFiles/lib/python', '--no-cache-dir', '--implementation', 'py', '--no-deps', '--upgrade']; - await Promise.all(requirements.map(async requirement => { - const success = await spawnAsync(process.env.CI_PYTHON_PATH || 'python3', args.concat(requirement)) - .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.concat(requirement)) - .catch(ex => console.error('Failed to install Python Libs using \'python\'', ex)); - } - })); -}); - -function spawnAsync(command, args) { - return new Promise((resolve, reject) => { - const proc = spawn(command, args, { cwd: __dirname }); - proc.stdout.on('data', data => { - // Log output on CI (else travis times out when there's not output). - if (isCI) { - console.log(data.toString()); - } - }); - proc.stderr.on('data', data => console.error(data.toString())); - proc.on('close', () => resolve()); - proc.on('error', error => reject(error)); - }); -} -function buildDatascienceDependencies() { - fsExtra.ensureDirSync(path.join(__dirname, 'tmp')); - spawn.sync('npm', ['run', 'dump-datascience-webpack-stats']); -} - -async function checkDatascienceDependencies() { - buildDatascienceDependencies(); - - 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 statsOutput = path.join(__dirname, 'tmp', 'ds-stats.json'); - const contents = await fsExtra.readFile(statsOutput).then(data => data.toString()); - const startIndex = contents.toString().indexOf('{') - 1; - - const json = JSON.parse(contents.substring(startIndex)); - 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.'); - } - json.chunks[0].modules.forEach(m => { - const name = m.name; - if (!name.startsWith('./node_modules')) { - return; - } - const nameWithoutNodeModules = name.substring('./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]}`; - - const matchedModules = modulesInPackageLock.filter(dependency => dependency === moduleName2 || dependency === moduleName1); - 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); - }); - - const errorMessages = []; - if (newModules.size > 0) { - errorMessages.push(`Add the untracked dependencies '${Array.from(newModules.values()).join(', ')}' to ${existingModulesFileName}`); - } - if (existingModulesCopy.size > 0) { - errorMessages.push(`Remove the unused '${Array.from(existingModulesCopy.values()).join(', ')}' dependencies from ${existingModulesFileName}`); - } - if (errorMessages.length > 0) { - throwAndLogError(errorMessages.join('\n')); - } -} -function throwAndLogError(message) { - if (message.length > 0) { - console.error(colors.red(message)); - throw new Error(message); - } -} -function hasNativeDependencies() { - let nativeDependencies = nativeDependencyChecker.check(path.join(__dirname, 'node_modules')); - if (!Array.isArray(nativeDependencies) || nativeDependencies.length === 0) { - return false; - } - const dependencies = JSON.parse(spawn.sync('npm', ['ls', '--json', '--prod']).stdout.toString()); - const jsonProperties = Object.keys(flat.flatten(dependencies)); - nativeDependencies = _.flatMap(nativeDependencies, item => path.dirname(item.substring(item.indexOf('node_modules') + 'node_modules'.length)).split(path.sep)) - .filter(item => item.length > 0) - .filter(item => jsonProperties.findIndex(flattenedDependency => flattenedDependency.endsWith(`dependencies.${item}.version`)) >= 0); - if (nativeDependencies.length > 0) { - console.error('Native dependencies detected', nativeDependencies); - return true; - } - return false; -} - -function buildDebugAdapterCoverage(done) { - const matches = glob.sync(path.join(__dirname, 'debug_coverage*/coverage.json')); - matches.forEach(coverageFile => { - const finalCoverageFile = path.join(path.dirname(coverageFile), 'coverage-final-upload.json'); - const remappedCollector = remapIstanbul.remap(JSON.parse(fs.readFileSync(coverageFile, 'utf8')), { - warn: warning => { - // We expect some warnings as any JS file without a typescript mapping will cause this. - // By default, we'll skip printing these to the console as it clutters it up. - console.warn(warning); - } - }); - - const reporter = new istanbul.Reporter(undefined, path.dirname(coverageFile)); - reporter.add('lcov'); - reporter.write(remappedCollector, true, () => { }); - }); - - done(); -} - -/** -* @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. -*/ - -const tsProjectMap = {}; -/** - * - * @param {hygieneOptions} options - */ -function getTsProject(options) { - const tsOptions = options.mode === 'compile' ? undefined : { strict: true, noImplicitAny: false, noImplicitThis: false }; - const mode = tsOptions && tsOptions.mode ? tsOptions.mode : ''; - return tsProjectMap[mode] ? tsProjectMap[mode] : tsProjectMap[mode] = ts.createProject('tsconfig.json', tsOptions); -} - -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) => { - 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' }, () => { }); -} +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* jshint node: true */ +/* jshint esversion: 6 */ + +'use strict'; + +const gulp = require('gulp'); +const ts = require('gulp-typescript'); +const spawn = require('cross-spawn'); +const path = require('path'); +const del = require('del'); +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'); +const os = require('os'); +const typescript = require('typescript'); + +const tsProject = ts.createProject('./tsconfig.json', { typescript }); + +const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; + +gulp.task('compileCore', (done) => { + let failed = false; + tsProject + .src() + .pipe(tsProject()) + .on('error', () => { + failed = true; + }) + .js.pipe(gulp.dest('out')) + .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); +}); + +gulp.task('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('compile', gulp.series('compileCore', 'compileApi')); + +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'])); +gulp.task('clean:vsix', () => del(['*.vsix'])); +gulp.task('clean:out', () => del(['out'])); + +gulp.task('clean', gulp.parallel('output:clean', 'clean:vsix', 'clean:out')); + +gulp.task('checkNativeDependencies', (done) => { + if (hasNativeDependencies()) { + done(new Error('Native dependencies detected')); + } + done(); +}); + +const webpackEnv = { NODE_OPTIONS: '--max_old_space_size=9096' }; + +async function buildWebPackForDevOrProduction(configFile, configNameForProductionBuilds) { + if (configNameForProductionBuilds) { + await buildWebPack(configNameForProductionBuilds, ['--config', configFile], webpackEnv); + } else { + await spawnAsync('npm', ['run', 'webpack', '--', '--config', configFile, '--mode', 'production'], webpackEnv); + } +} +gulp.task('webpack', async () => { + // Build node_modules. + await buildWebPackForDevOrProduction('./build/webpack/webpack.extension.dependencies.config.js', 'production'); + await buildWebPackForDevOrProduction('./build/webpack/webpack.extension.config.js', 'extension'); + 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); +}); + +async function updateBuildNumber(args) { + if (args && args.buildNumber) { + // Edit the version number from the package.json + const packageJsonContents = await fsExtra.readFile('package.json', 'utf-8'); + const packageJson = JSON.parse(packageJsonContents); + + // Change version number + const versionParts = packageJson.version.split('.'); + const buildNumberPortion = + versionParts.length > 2 ? versionParts[2].replace(/(\d+)/, args.buildNumber) : args.buildNumber; + const newVersion = + versionParts.length > 1 + ? `${versionParts[0]}.${versionParts[1]}.${buildNumberPortion}` + : packageJson.version; + packageJson.version = newVersion; + + // Write back to the package json + await fsExtra.writeFile('package.json', JSON.stringify(packageJson, null, 4), 'utf-8'); + + // Update the changelog.md if we are told to (this should happen on the release branch) + if (args.updateChangelog) { + const changeLogContents = await fsExtra.readFile('CHANGELOG.md', 'utf-8'); + const fixedContents = changeLogContents.replace( + /##\s*(\d+)\.(\d+)\.(\d+)\s*\(/, + `## $1.$2.${buildNumberPortion} (`, + ); + + // Write back to changelog.md + await fsExtra.writeFile('CHANGELOG.md', fixedContents, 'utf-8'); + } + } else { + throw Error('buildNumber argument required for updateBuildNumber task'); + } +} + +async function buildWebPack(webpackConfigName, args, env) { + // Remember to perform a case insensitive search. + const allowedWarnings = getAllowedWarningsForWebPack(webpackConfigName).map((item) => item.toLowerCase()); + const stdOut = await spawnAsync( + 'npm', + ['run', 'webpack', '--', ...args, ...['--mode', 'production', '--devtool', 'source-map']], + env, + ); + const stdOutLines = stdOut + .split(os.EOL) + .map((item) => item.trim()) + .filter((item) => item.length > 0); + // Remember to perform a case insensitive search. + const warnings = stdOutLines + .filter((item) => item.startsWith('WARNING in ')) + .filter( + (item) => + allowedWarnings.findIndex((allowedWarning) => + item.toLowerCase().startsWith(allowedWarning.toLowerCase()), + ) === -1, + ); + const errors = stdOutLines.some((item) => item.startsWith('ERROR in')); + if (errors) { + throw new Error(`Errors in ${webpackConfigName}, \n${warnings.join(', ')}\n\n${stdOut}`); + } + if (warnings.length > 0) { + throw new Error( + `Warnings in ${webpackConfigName}, Check gulpfile.js to see if the warning should be allowed., \n\n${stdOut}`, + ); + } +} +function getAllowedWarningsForWebPack(buildConfig) { + switch (buildConfig) { + case 'production': + return [ + 'WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).', + 'WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.', + 'WARNING in webpack performance recommendations:', + 'WARNING in ./node_modules/encoding/lib/iconv-loader.js', + 'WARNING in ./node_modules/any-promise/register.js', + 'WARNING in ./node_modules/diagnostic-channel-publishers/dist/src/azure-coretracing.pub.js', + 'WARNING in ./node_modules/applicationinsights/out/AutoCollection/NativePerformance.js', + ]; + case 'extension': + return [ + 'WARNING in ./node_modules/encoding/lib/iconv-loader.js', + 'WARNING in ./node_modules/any-promise/register.js', + 'remove-files-plugin@1.4.0:', + 'WARNING in ./node_modules/diagnostic-channel-publishers/dist/src/azure-coretracing.pub.js', + 'WARNING in ./node_modules/applicationinsights/out/AutoCollection/NativePerformance.js', + ]; + case 'debugAdapter': + return [ + 'WARNING in ./node_modules/vscode-uri/lib/index.js', + 'WARNING in ./node_modules/diagnostic-channel-publishers/dist/src/azure-coretracing.pub.js', + 'WARNING in ./node_modules/applicationinsights/out/AutoCollection/NativePerformance.js', + ]; + 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('verifyBundle', async () => { + const matches = await glob.sync(path.join(__dirname, '*.vsix')); + if (!matches || matches.length === 0) { + throw new Error('Bundle does not exist'); + } else { + console.log(`Bundle ${matches[0]} exists.`); + } +}); + +gulp.task('prePublishBundle', gulp.series('webpack')); +gulp.task('checkDependencies', gulp.series('checkNativeDependencies')); +gulp.task('prePublishNonBundle', gulp.series('compile')); + +function spawnAsync(command, args, env, rejectOnStdErr = false) { + env = env || {}; + env = { ...process.env, ...env }; + return new Promise((resolve, reject) => { + let stdOut = ''; + console.info(`> ${command} ${args.join(' ')}`); + const proc = spawn(command, args, { cwd: __dirname, env }); + proc.stdout.on('data', (data) => { + // Log output on CI (else travis times out when there's not output). + stdOut += data.toString(); + if (isCI) { + console.log(data.toString()); + } + }); + proc.stderr.on('data', (data) => { + console.error(data.toString()); + if (rejectOnStdErr) { + reject(data.toString()); + } + }); + proc.on('close', () => resolve(stdOut)); + proc.on('error', (error) => reject(error)); + }); +} + +function hasNativeDependencies() { + let nativeDependencies = nativeDependencyChecker.check(path.join(__dirname, 'node_modules')); + if (!Array.isArray(nativeDependencies) || nativeDependencies.length === 0) { + return false; + } + const dependencies = JSON.parse(spawn.sync('npm', ['ls', '--json', '--prod']).stdout.toString()); + const jsonProperties = Object.keys(flat.flatten(dependencies)); + nativeDependencies = _.flatMap(nativeDependencies, (item) => + path.dirname(item.substring(item.indexOf('node_modules') + 'node_modules'.length)).split(path.sep), + ) + .filter((item) => item.length > 0) + .filter((item) => item !== 'fsevents') + .filter( + (item) => + jsonProperties.findIndex((flattenedDependency) => + flattenedDependency.endsWith(`dependencies.${item}.version`), + ) >= 0, + ); + if (nativeDependencies.length > 0) { + console.error('Native dependencies detected', nativeDependencies); + return true; + } + return false; +} diff --git a/images/ConfigureDebugger.gif b/images/ConfigureDebugger.gif new file mode 100644 index 000000000000..41113d65896d Binary files /dev/null and b/images/ConfigureDebugger.gif differ diff --git a/images/ConfigureTests.gif b/images/ConfigureTests.gif new file mode 100644 index 000000000000..38ae2db551e1 Binary files /dev/null and b/images/ConfigureTests.gif differ diff --git a/images/InterpreterSelectionZoom.gif b/images/InterpreterSelectionZoom.gif new file mode 100644 index 000000000000..dc5db03aad3d Binary files /dev/null and b/images/InterpreterSelectionZoom.gif differ diff --git a/images/JavascriptProfiler.png b/images/JavascriptProfiler.png new file mode 100644 index 000000000000..f26e1480c021 Binary files /dev/null and b/images/JavascriptProfiler.png differ diff --git a/images/OpenOrCreateNotebook.gif b/images/OpenOrCreateNotebook.gif new file mode 100644 index 000000000000..a0957d415d7d Binary files /dev/null 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 new file mode 100644 index 000000000000..ce0c81676c09 Binary files /dev/null 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/interactive.gif b/images/interactive.gif new file mode 100644 index 000000000000..f8080fa94576 Binary files /dev/null and b/images/interactive.gif differ diff --git a/images/kernelchange.gif b/images/kernelchange.gif new file mode 100644 index 000000000000..d2b753b84c09 Binary files /dev/null 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 new file mode 100644 index 000000000000..a3c438b761e0 Binary files /dev/null and b/images/plotviewer.gif differ diff --git a/images/remoteserver.gif b/images/remoteserver.gif new file mode 100644 index 000000000000..f979d557aa6b Binary files /dev/null and b/images/remoteserver.gif differ diff --git a/images/runbyline.gif b/images/runbyline.gif new file mode 100644 index 000000000000..1c0679f9a458 Binary files /dev/null and b/images/runbyline.gif differ diff --git a/images/savetopythonfile.png b/images/savetopythonfile.png new file mode 100644 index 000000000000..e4a7f08d3db0 Binary files /dev/null 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 new file mode 100644 index 000000000000..31197571b796 Binary files /dev/null and b/images/variableexplorer.png differ diff --git a/news/1 Enhancements/120.md b/news/1 Enhancements/120.md deleted file mode 100644 index b42e28ea4166..000000000000 --- a/news/1 Enhancements/120.md +++ /dev/null @@ -1,2 +0,0 @@ -Create diagnostics for failed/skipped tests that were run with pytest. -(thanks [Chris NeJame](https://github.com/SalmonMode/)) diff --git a/news/1 Enhancements/2906.md b/news/1 Enhancements/2906.md deleted file mode 100644 index 209e19264d78..000000000000 --- a/news/1 Enhancements/2906.md +++ /dev/null @@ -1,2 +0,0 @@ -Use Pylint message names instead of codes -(thanks to [Roman Kornev](https://github.com/RomanKornev/)) diff --git a/news/1 Enhancements/3284.md b/news/1 Enhancements/3284.md deleted file mode 100644 index 20c77192c5fe..000000000000 --- a/news/1 Enhancements/3284.md +++ /dev/null @@ -1 +0,0 @@ -Indent on enter after line continuations. diff --git a/news/1 Enhancements/3321.md b/news/1 Enhancements/3321.md deleted file mode 100644 index a51cefe4a567..000000000000 --- a/news/1 Enhancements/3321.md +++ /dev/null @@ -1 +0,0 @@ -Prompt user to select a debug configuration when generating the `launch.json`. diff --git a/news/1 Enhancements/3369.md b/news/1 Enhancements/3369.md deleted file mode 100644 index 8fc39b4319c5..000000000000 --- a/news/1 Enhancements/3369.md +++ /dev/null @@ -1 +0,0 @@ -Improvements to automatic selection of the python interpreter. diff --git a/news/1 Enhancements/3597.md b/news/1 Enhancements/3597.md deleted file mode 100644 index 728727722619..000000000000 --- a/news/1 Enhancements/3597.md +++ /dev/null @@ -1,2 +0,0 @@ -Add support for column numbers for problems returned by `mypy`. -(thanks [Eric Traut](https://github.com/erictraut)) diff --git a/news/1 Enhancements/3634.md b/news/1 Enhancements/3634.md deleted file mode 100644 index e43f1926b7d6..000000000000 --- a/news/1 Enhancements/3634.md +++ /dev/null @@ -1 +0,0 @@ -Display actionable message when LS is not supported \ No newline at end of file diff --git a/news/1 Enhancements/3641.md b/news/1 Enhancements/3641.md deleted file mode 100644 index d0df84a033a5..000000000000 --- a/news/1 Enhancements/3641.md +++ /dev/null @@ -1 +0,0 @@ -Make sure we are looking for conda in all the right places diff --git a/news/1 Enhancements/3659.md b/news/1 Enhancements/3659.md deleted file mode 100644 index 409fe34b273a..000000000000 --- a/news/1 Enhancements/3659.md +++ /dev/null @@ -1 +0,0 @@ -Improvements to message displayed when linter is not installed \ No newline at end of file diff --git a/news/1 Enhancements/3668.md b/news/1 Enhancements/3668.md deleted file mode 100644 index ee19deadb488..000000000000 --- a/news/1 Enhancements/3668.md +++ /dev/null @@ -1 +0,0 @@ -Add the Jupyter Server URI to the Interactive Window info cell \ 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 ecc51777759d..000000000000 --- a/news/1 Enhancements/README.md +++ /dev/null @@ -1 +0,0 @@ -Changes that add new features. diff --git a/news/2 Fixes/2571.md b/news/2 Fixes/2571.md deleted file mode 100644 index 365e684b43ab..000000000000 --- a/news/2 Fixes/2571.md +++ /dev/null @@ -1,2 +0,0 @@ -Fix bug affecting multiple linters used in a workspace. -(thanks [Ilia Novoselov](https://github.com/nullie)) diff --git a/news/2 Fixes/3362.md b/news/2 Fixes/3362.md deleted file mode 100644 index 6ddf6d0e5324..000000000000 --- a/news/2 Fixes/3362.md +++ /dev/null @@ -1 +0,0 @@ -Add support for running an entire file in the Python Interactive window diff --git a/news/2 Fixes/3419.md b/news/2 Fixes/3419.md deleted file mode 100644 index 42b8f94c2775..000000000000 --- a/news/2 Fixes/3419.md +++ /dev/null @@ -1 +0,0 @@ -When in multi-root workspace, store selected python path in the `settings.json` file of the workspace folder. diff --git a/news/2 Fixes/3529.md b/news/2 Fixes/3529.md deleted file mode 100644 index 94226f0217c5..000000000000 --- a/news/2 Fixes/3529.md +++ /dev/null @@ -1 +0,0 @@ -Fix console wrapping in output so that console based status bars and spinners work. diff --git a/news/2 Fixes/3674.md b/news/2 Fixes/3674.md deleted file mode 100644 index c6bd52d73853..000000000000 --- a/news/2 Fixes/3674.md +++ /dev/null @@ -1 +0,0 @@ -Fixed tests related to the `onEnter` format provider. diff --git a/news/2 Fixes/3693.md b/news/2 Fixes/3693.md deleted file mode 100644 index 37241de43743..000000000000 --- a/news/2 Fixes/3693.md +++ /dev/null @@ -1 +0,0 @@ -Lowering threshold for Language Server support on a platform. diff --git a/news/2 Fixes/3699.md b/news/2 Fixes/3699.md deleted file mode 100644 index df18b4861e43..000000000000 --- a/news/2 Fixes/3699.md +++ /dev/null @@ -1 +0,0 @@ -Survive missing kernelspecs as a default will be created. diff --git a/news/2 Fixes/3734.md b/news/2 Fixes/3734.md deleted file mode 100644 index a832ad019a41..000000000000 --- a/news/2 Fixes/3734.md +++ /dev/null @@ -1 +0,0 @@ -Activate the extension when loading ipynb files diff --git a/news/2 Fixes/3749.md b/news/2 Fixes/3749.md deleted file mode 100644 index 7e1ce9562ab7..000000000000 --- a/news/2 Fixes/3749.md +++ /dev/null @@ -1 +0,0 @@ -Don't restart the Jupyter server on any settings change. Also don't throw interpreter changed events on unrelated settings changes. \ No newline at end of file diff --git a/news/2 Fixes/3757.md b/news/2 Fixes/3757.md deleted file mode 100644 index a058980131a6..000000000000 --- a/news/2 Fixes/3757.md +++ /dev/null @@ -1 +0,0 @@ -Support whitespace (tabs and spaces) in output diff --git a/news/2 Fixes/3767.md b/news/2 Fixes/3767.md deleted file mode 100644 index 4a8ad9d48765..000000000000 --- a/news/2 Fixes/3767.md +++ /dev/null @@ -1 +0,0 @@ -Ensure file names are not captured when sending telemetry for unit tests. diff --git a/news/2 Fixes/3775.md b/news/2 Fixes/3775.md deleted file mode 100644 index 6b1bf4bbd539..000000000000 --- a/news/2 Fixes/3775.md +++ /dev/null @@ -1 +0,0 @@ -Address problem with Python Interactive icons not working in insider's build. VS Code is more restrictive on what files can load in a webview. diff --git a/news/2 Fixes/3824.md b/news/2 Fixes/3824.md deleted file mode 100644 index d490d6ff015c..000000000000 --- a/news/2 Fixes/3824.md +++ /dev/null @@ -1 +0,0 @@ -Fix output so that it wraps '<' entries in to allow html like tags to be output. diff --git a/news/2 Fixes/3856.md b/news/2 Fixes/3856.md deleted file mode 100644 index 4b5c24fbf726..000000000000 --- a/news/2 Fixes/3856.md +++ /dev/null @@ -1 +0,0 @@ -Keep the Jupyter remote server URI input box open so you can copy and paste into it easier \ No newline at end of file diff --git a/news/2 Fixes/3925.md b/news/2 Fixes/3925.md deleted file mode 100644 index f049ed3c003b..000000000000 --- a/news/2 Fixes/3925.md +++ /dev/null @@ -1 +0,0 @@ -Clean up command names for data science \ No newline at end of file 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/1521.md b/news/3 Code Health/1521.md deleted file mode 100644 index 7a3f68b59fe7..000000000000 --- a/news/3 Code Health/1521.md +++ /dev/null @@ -1 +0,0 @@ -Created system test to ensure terminal gets activated with anaconda environment \ No newline at end of file diff --git a/news/3 Code Health/1522.md b/news/3 Code Health/1522.md deleted file mode 100644 index d0226766eef9..000000000000 --- a/news/3 Code Health/1522.md +++ /dev/null @@ -1 +0,0 @@ -Added system tests to ensure terminal gets activated with virtualenv environment diff --git a/news/3 Code Health/1523.md b/news/3 Code Health/1523.md deleted file mode 100644 index ac9d6c029eba..000000000000 --- a/news/3 Code Health/1523.md +++ /dev/null @@ -1 +0,0 @@ -Added system test to ensure terminal gets activated with pipenv \ No newline at end of file diff --git a/news/3 Code Health/2339.md b/news/3 Code Health/2339.md deleted file mode 100644 index 8ccee3d001f3..000000000000 --- a/news/3 Code Health/2339.md +++ /dev/null @@ -1 +0,0 @@ -Fix flaky tests related to auto selection of virtual environments. diff --git a/news/3 Code Health/3084.md b/news/3 Code Health/3084.md deleted file mode 100644 index 44edfb616546..000000000000 --- a/news/3 Code Health/3084.md +++ /dev/null @@ -1 +0,0 @@ -Add tests for clicking buttons in history pane diff --git a/news/3 Code Health/3087.md b/news/3 Code Health/3087.md deleted file mode 100644 index 1a5478360710..000000000000 --- a/news/3 Code Health/3087.md +++ /dev/null @@ -1 +0,0 @@ -Add tests for clear and delete buttons in the history pane diff --git a/news/3 Code Health/3092.md b/news/3 Code Health/3092.md deleted file mode 100644 index 1975e44388dd..000000000000 --- a/news/3 Code Health/3092.md +++ /dev/null @@ -1 +0,0 @@ -Add tests for clicking buttons on individual cells diff --git a/news/3 Code Health/3555.md b/news/3 Code Health/3555.md deleted file mode 100644 index 34d1cddb167a..000000000000 --- a/news/3 Code Health/3555.md +++ /dev/null @@ -1 +0,0 @@ -Update our CI/nightly full build to a YAML definition build in Azure DevOps. diff --git a/news/3 Code Health/3556.md b/news/3 Code Health/3556.md deleted file mode 100644 index c0b0951c1bf8..000000000000 --- a/news/3 Code Health/3556.md +++ /dev/null @@ -1 +0,0 @@ -Add mock of Jupyter API to allow functional tests to run more quickly and more consistently. diff --git a/news/3 Code Health/3682.md b/news/3 Code Health/3682.md deleted file mode 100644 index 0eb40c73eb34..000000000000 --- a/news/3 Code Health/3682.md +++ /dev/null @@ -1 +0,0 @@ -Fix the timeout for DataScience functional tests \ No newline at end of file diff --git a/news/3 Code Health/3684.md b/news/3 Code Health/3684.md deleted file mode 100644 index f200fa0fbef7..000000000000 --- a/news/3 Code Health/3684.md +++ /dev/null @@ -1 +0,0 @@ -Fixed language server smoke tests. diff --git a/news/3 Code Health/3714.md b/news/3 Code Health/3714.md deleted file mode 100644 index f8a8b4db7648..000000000000 --- a/news/3 Code Health/3714.md +++ /dev/null @@ -1 +0,0 @@ -Add a functional test for interactive window remote connect scenario \ No newline at end of file diff --git a/news/3 Code Health/3781.md b/news/3 Code Health/3781.md deleted file mode 100644 index acbe0a2296b0..000000000000 --- a/news/3 Code Health/3781.md +++ /dev/null @@ -1 +0,0 @@ -Remove `src/server` folder, as this is no longer required. diff --git a/news/3 Code Health/3837.md b/news/3 Code Health/3837.md deleted file mode 100644 index 401fa7db6100..000000000000 --- a/news/3 Code Health/3837.md +++ /dev/null @@ -1,3 +0,0 @@ -Bugfix to `pvsc-dev-ext.py` where arguments to git would not be passed on POSIX-based environments. Extended `pvsc-dev-ext.py setup` command with 2 -optional flags-- `--repo` and `--branch` to override the default git repository URL and the branch used to clone and install the extension. -(thanks [Anthony Shaw](https://github.com/tonybaloney/)) diff --git a/news/3 Code Health/3899.md b/news/3 Code Health/3899.md deleted file mode 100644 index e38295468dea..000000000000 --- a/news/3 Code Health/3899.md +++ /dev/null @@ -1 +0,0 @@ -Improvements to execution times of CI on Travis. diff --git a/news/3 Code Health/3916.md b/news/3 Code Health/3916.md deleted file mode 100644 index b6795ade8778..000000000000 --- a/news/3 Code Health/3916.md +++ /dev/null @@ -1 +0,0 @@ -Make sure to search for the best python when launching the non default interpreter. 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 24363e76037e..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](https://en.wikipedia.org/wiki/42_(number)) -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 1c36f0a2e49e..000000000000 --- a/news/announce.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -"""Generate the changelog. - -Usage: announce [--dry_run | --interim | --final] [<directory>] - -""" -import dataclasses -import enum -import operator -import os -import pathlib -import re -import subprocess -import sys - -import docopt - - -FILENAME_RE = re.compile(r"(?P<issue>\d+)(?P<nonce>-\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) - return ENTRY_TEMPLATE.format( - entry=entry.description.strip(), issue=entry.issue_number, issue_url=issue_url - ) - - -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=True, - 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 main(run_type, directory): - directory = pathlib.Path(directory) - data = gather(directory) - markdown = changelog_markdown(data) - if run_type != RunType.dry_run: - 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["<directory>"] or pathlib.Path(__file__).parent - main(run_type, directory) diff --git a/news/requirements.txt b/news/requirements.txt deleted file mode 100644 index 0dbf42bfb455..000000000000 --- a/news/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -docopt~=0.6.2 -pytest~=3.4.1 diff --git a/news/test_announce.py b/news/test_announce.py deleted file mode 100644 index 23e637e04834..000000000000 --- a/news/test_announce.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import codecs -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 - - -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["<directory>"] == "./news" - args = docopt.docopt(ann.__doc__, ["--dry_run", "./news"]) - assert args["--dry_run"] - assert args["<directory>"] == "./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 302ad6ddb3cb..82053df77576 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16832 +1,26307 @@ { "name": "python", - "version": "2019.1.0-alpha", - "lockfileVersion": 1, + "version": "2026.5.0-dev", + "lockfileVersion": 2, "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" + "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": "^14.0.0", + "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" } }, - "@babel/core": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.1.0.tgz", - "integrity": "sha512-9EWmD0cQAbcXSc+31RIoYgEHx3KQ2CCSMDBhnXrShWvo45TMw+3/55KVxlhkG53kw9tl87DqINgHDgFVhZJV/Q==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.0.0", - "@babel/helpers": "^7.1.0", - "@babel/parser": "^7.1.0", - "@babel/template": "^7.1.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0", - "convert-source-map": "^1.1.0", - "debug": "^3.1.0", - "json5": "^0.5.0", - "lodash": "^4.17.10", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "debug": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", - "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - } + "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/generator": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0.tgz", - "integrity": "sha512-/BM2vupkpbZXq22l1ALO7MqXJZH2k8bKVv8Y+pABFnzWdztDB/ZLveP5At21vLz5c2YtSE6p7j2FZEsqafMz5Q==", + "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/types": "^7.0.0", - "jsesc": "^2.5.1", - "lodash": "^4.17.10", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" }, + "engines": { + "node": ">=6.0.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": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - } + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.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/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": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" } }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", - "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", + "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==" + }, + "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.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.0.0.tgz", - "integrity": "sha512-ebJ2JM6NAKW0fQEqN8hOLxK84RbRz9OkUhGS/Xd5u56ejMfVbayJ4+LykERZCOUM6faa6Fp3SZNX3fcT16MKHw==", + "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.0.0", - "esutils": "^2.0.0" + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "@babel/helper-call-delegate": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.1.0.tgz", - "integrity": "sha512-YEtYZrw3GUK6emQHKthltKNZwszBcHK58Ygcis+gVUrF4/FmTVr5CCqQNSfmvg2y+YDEANyYoaLz/SHsnusCwQ==", + "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.0.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "@babel/helper-define-map": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz", - "integrity": "sha512-yPPcW8dc3gZLN+U1mhYV91QU3n5uTbx7DUdf8NnPbjS0RMwBuHi9Xt2MUgppmNz7CJxTBWsGczTiEp1CSOTPRg==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/types": "^7.0.0", - "lodash": "^4.17.10" + "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" + } + }, + "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" + } + }, + "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": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } } }, - "@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/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-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/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "@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/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" } }, - "@babel/helper-hoist-variables": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0.tgz", - "integrity": "sha512-Ggv5sldXUeSKsuzLkddtyhyHe2YantsxWKNi7A+7LeD12ExRDWTRk29JCXpaHPAbMaIPZSil7n+lq78WY2VY7w==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" + "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==", + "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, - "requires": { - "@babel/types": "^7.0.0" + "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.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.1.0.tgz", - "integrity": "sha512-0JZRd2yhawo79Rcm4w0LwSMILFmFXjugG3yqf+P/UsKsRS1mJCmMwwlHDlMg7Avr9LrvSpp4ZSULO9r8jpCzcw==", + "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.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0", - "lodash": "^4.17.10" - }, "dependencies": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - } + "@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.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.0.0.tgz", - "integrity": "sha512-TR0/N0NDCcUIUEbqV6dCO+LptmmSQFQ7q70lfcEB4URsjD0E1HzicrwUH+ap6BAQ2jhCX9Q4UqZy4wilujWlkg==", - "dev": true, - "requires": { - "lodash": "^4.17.10" + "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" + } + }, + "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==" + }, + "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, "dependencies": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - } + "@azure/msal-common": "14.10.0" + }, + "engines": { + "node": ">=0.8.0" } }, - "@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==", + "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/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" + "engines": { + "node": ">=0.8.0" } }, - "@babel/helper-replace-supers": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.1.0.tgz", - "integrity": "sha512-BvcDWYZRWVuDeXTYZWxekQNO5D4kO55aArwZOTFXw6rlLQA8ZaDicJR1sO47h+HrnCiDFiww0fSPV0d713KBGQ==", + "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-member-expression-to-functions": "^7.0.0", - "@babel/helper-optimise-call-expression": "^7.0.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" + "dependencies": { + "@azure/msal-common": "14.12.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" } }, - "@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-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.1.0", - "@babel/types": "^7.0.0" + "engines": { + "node": ">=0.8.0" } }, - "@babel/helper-split-export-declaration": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz", - "integrity": "sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==", + "node_modules/@azure/msal-node/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==", "dev": true, - "requires": { - "@babel/types": "^7.0.0" + "bin": { + "uuid": "dist/bin/uuid" } }, - "@babel/helper-wrap-function": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.1.0.tgz", - "integrity": "sha512-R6HU3dete+rwsdAfrOzTlE9Mcpk4RjU3aX3gi9grtmugQY0u79X7eogUvfXA5sI81Mfq1cn6AgxihfN33STjJA==", + "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" + } + }, + "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==" + }, + "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-function-name": "^7.1.0", - "@babel/template": "^7.1.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.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/helpers": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.1.0.tgz", - "integrity": "sha512-V1jXUTNdTpBn37wqqN73U+eBpzlLHmxA4aDaghJBggmzly/FpIJMHXse9lgdzQQT4gs5jZ5NmYxOL8G3ROc29g==", + "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/template": "^7.1.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" + "engines": { + "node": ">=6.9.0" } }, - "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "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": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.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" + } + }, + "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, "dependencies": { - "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" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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 - }, + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { "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" - } + "optional": true } } }, - "@babel/parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.1.0.tgz", - "integrity": "sha512-SmjnXCuPAlai75AFtzv+KCBcJ3sDDWbIn+WytKw1k+wAtEy6phqI2RqKh/zAnw53i1NR8su3Ep/UoqaKcimuLg==", - "dev": true - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.1.0.tgz", - "integrity": "sha512-Fq803F3Jcxo20MXUSDdmZZXrPe6BWyGcWBPPNB/M7WaUYESKDeKMOGIxEzQOjGSmW/NWb6UaPZrtTB2ekhB/ew==", + "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/helper-remap-async-to-generator": "^7.1.0", - "@babel/plugin-syntax-async-generators": "^7.0.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-json-strings": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.0.0.tgz", - "integrity": "sha512-kfVdUkIAGJIVmHmtS/40i/fg/AGnw/rsZBCaapY5yjeO5RA9m165Xbw9KMOu2nqXP5dTFjEjHdfNdoVcHv133Q==", + "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/plugin-syntax-json-strings": "^7.0.0" + "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-proposal-object-rest-spread": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.0.0.tgz", - "integrity": "sha512-14fhfoPcNu7itSen7Py1iGN0gEm87hX/B+8nZPqkdmANyyYWYMY2pjA3r8WXbWVKMzfnSNS0xY8GVS0IjXi/iw==", + "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", - "@babel/plugin-syntax-object-rest-spread": "^7.0.0" + "dependencies": { + "yallist": "^3.0.2" } }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.0.0.tgz", - "integrity": "sha512-JPqAvLG1s13B/AuoBjdBYvn38RqW6n1TzrQO839/sIpqLpbnXKacsAgpZHzLD83Sm8SDXMkkrAvEnJ25+0yIpw==", + "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 + }, + "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", - "@babel/plugin-syntax-optional-catch-binding": "^7.0.0" + "engines": { + "node": ">=6.9.0" } }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.0.0.tgz", - "integrity": "sha512-tM3icA6GhC3ch2SkmSxv7J/hCWKISzwycub6eGsDrFDgukD4dZ/I+x81XgW0YslS6mzNuQ1Cbzh5osjIMgepPQ==", + "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", - "@babel/helper-regex": "^7.0.0", - "regexpu-core": "^4.2.0" + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/plugin-syntax-async-generators": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.0.0.tgz", - "integrity": "sha512-im7ged00ddGKAjcZgewXmp1vxSZQQywuQXe2B1A7kajjZmDeY/ekMPmWr9zJgveSaQH0k7BcGrojQhcK06l0zA==", + "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-json-strings": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.0.0.tgz", - "integrity": "sha512-UlSfNydC+XLj4bw7ijpldc1uZ/HB84vw+U6BTuqMdIEmz/LDe63w/GHtpQMdXWdqQZFeAI9PjnHe/vDhwirhKA==", + "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-syntax-jsx": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.0.0.tgz", - "integrity": "sha512-PdmL2AoPsCLWxhIr3kG2+F9v4WH06Q3z+NoGVpQgnUNGcagXHq5sB3OXxkSahKq9TLdNMN/AJzFYSOo8UKDMHg==", + "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-syntax-object-rest-spread": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.0.0.tgz", - "integrity": "sha512-5A0n4p6bIiVe5OvQPxBnesezsgFJdHhSs3uFSvaPdMqtsovajLZ+G2vZyvNe10EzJBWWo3AcHGKhAFUxqwp2dw==", + "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-plugin-utils": "^7.0.0" + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.0.0.tgz", - "integrity": "sha512-Wc+HVvwjcq5qBg1w5RG9o9RVzmCaAg/Vp0erHCKpAYV8La6I94o4GQAmFYNmkzoMO6gzoOSulpKeSSz6mPEoZw==", + "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-arrow-functions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.0.0.tgz", - "integrity": "sha512-2EZDBl1WIO/q4DIkIp4s86sdp4ZifL51MoIviLY/gG/mLSuOIEg7J8o6mhbxOTvUJkaN50n+8u41FVsr5KLy/w==", + "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" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.1.0.tgz", - "integrity": "sha512-rNmcmoQ78IrvNCIt/R9U+cixUHeYAzgusTFgIAv+wQb9HJU4szhpDD6e5GCACmj/JP5KxuCwM96bX3L9v4ZN/g==", + "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-module-imports": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-remap-async-to-generator": "^7.1.0" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.0.0.tgz", - "integrity": "sha512-AOBiyUp7vYTqz2Jibe1UaAWL0Hl9JUXEgjFvvvcSc9MVDItv46ViXFw2F7SVt1B5k+KWjl44eeXOAk3UDEaJjQ==", + "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-block-scoping": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.0.0.tgz", - "integrity": "sha512-GWEMCrmHQcYWISilUrk9GDqH4enf3UmhOEbNbNrlNAX1ssH3MsS1xLOS6rdjRVPgA7XXVPn87tRkdTEoA/dxEg==", + "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", - "lodash": "^4.17.10" - }, + "license": "MIT", "dependencies": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - } + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/plugin-transform-classes": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.1.0.tgz", - "integrity": "sha512-rNaqoD+4OCBZjM7VaskladgqnZ1LO6o2UxuWSDzljzW21pN1KXkB7BstAVweZdxQkHAujps5QMNOTWesBciKFg==", + "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-annotate-as-pure": "^7.0.0", - "@babel/helper-define-map": "^7.1.0", - "@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.1.0", - "@babel/helper-split-export-declaration": "^7.0.0", - "globals": "^11.1.0" + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, - "@babel/plugin-transform-computed-properties": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.0.0.tgz", - "integrity": "sha512-ubouZdChNAv4AAWAgU7QKbB93NU5sHwInEWfp+/OzJKA02E6Woh9RVoX4sZrbRwtybky/d7baTUqwFx+HgbvMA==", + "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-destructuring": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.0.0.tgz", - "integrity": "sha512-Fr2GtF8YJSXGTyFPakPFB4ODaEKGU04bPsAllAIabwoXdFrPxL0LVXQX5dQWoxOjjgozarJcC9eWGsj0fD6Zsg==", + "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-plugin-utils": "^7.0.0" + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.30.2" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.0.0.tgz", - "integrity": "sha512-00THs8eJxOJUFVx1w8i1MBF4XH4PsAjKjQ1eqN/uCH3YKwP21GCKfrn6YZFZswbOk9+0cw1zGQPHVc1KBlSxig==", + "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", - "@babel/helper-regex": "^7.0.0", - "regexpu-core": "^4.1.3" + "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-duplicate-keys": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.0.0.tgz", - "integrity": "sha512-w2vfPkMqRkdxx+C71ATLJG30PpwtTpW7DDdLqYt2acXU7YjztzeWW2Jk1T6hKqCLYCcEA5UQM/+xTAm+QCSnuQ==", + "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-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-exponentiation-operator": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.1.0.tgz", - "integrity": "sha512-uZt9kD1Pp/JubkukOGQml9tqAeI8NkE98oZnHZ2qHRElmeKCodbTZgOEUtujSCSLhHSBWbzNiFSDIMC4/RBTLQ==", + "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-builder-binary-assignment-operator-visitor": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "@babel/plugin-transform-for-of": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.0.0.tgz", - "integrity": "sha512-TlxKecN20X2tt2UEr2LNE6aqA0oPeMT1Y3cgz8k4Dn1j5ObT8M3nl9aA37LLklx0PBZKETC9ZAf9n/6SujTuXA==", + "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-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.1.0.tgz", - "integrity": "sha512-VxOa1TMlFMtqPW2IDYZQaHsFrq/dDoIjgN098NowhexhZcz3UGlvPgZXuE1jEvNygyWyxRacqDpCZt+par1FNg==", + "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-function-name": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" + "engines": { + "node": ">= 12" } }, - "@babel/plugin-transform-literals": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.0.0.tgz", - "integrity": "sha512-1NTDBWkeNXgpUcyoVFxbr9hS57EpZYXpje92zv0SUzjdu3enaRwF/l3cmyRnXLtIdyJASyiS6PtybK+CgKf7jA==", + "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-plugin-utils": "^7.0.0" + "dependencies": { + "@cspotcode/source-map-consumer": "0.8.0" + }, + "engines": { + "node": ">=12" } }, - "@babel/plugin-transform-modules-amd": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.1.0.tgz", - "integrity": "sha512-wt8P+xQ85rrnGNr2x1iV3DW32W8zrB6ctuBkYBbf5/ZzJY99Ob4MFgsZDFgczNU76iy9PWsy4EuxOliDjdKw6A==", + "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-module-transforms": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" + "engines": { + "node": ">=10.0.0" } }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.1.0.tgz", - "integrity": "sha512-wtNwtMjn1XGwM0AXPspQgvmE6msSJP15CX2RVfpTSTNPLhKhaOjaIfBaVfj4iUZ/VrFSodcFedwtPg/NxwQlPA==", + "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", - "@babel/helper-simple-access": "^7.1.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-modules-systemjs": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.0.0.tgz", - "integrity": "sha512-8EDKMAsitLkiF/D4Zhe9CHEE2XLh4bfLbb9/Zf3FgXYQOZyZYyg7EAel/aT2A7bHv62jwHf09q2KU/oEexr83g==", + "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": { - "@babel/helper-hoist-variables": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0" + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "@babel/plugin-transform-modules-umd": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.1.0.tgz", - "integrity": "sha512-enrRtn5TfRhMmbRwm7F8qOj0qEYByqUvTttPEGimcBH4CJHphjyK1Vg7sdU7JjeEmgSpM890IT/efS2nMHwYig==", + "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-module-transforms": "^7.1.0", - "@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-new-target": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz", - "integrity": "sha512-yin069FYjah+LbqfGeTfzIBODex/e++Yfa0rH0fpfam9uTbuEeEOx5GLGr210ggOV77mVRNoeqSYqeuaqSzVSw==", + "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" - } + "license": "Python-2.0" }, - "@babel/plugin-transform-object-super": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.1.0.tgz", - "integrity": "sha512-/O02Je1CRTSk2SSJaq0xjwQ8hG4zhZGNjE8psTsSNPXyLRCODv7/PBozqT5AmQMzp7MI3ndvMhGdqp9c96tTEw==", + "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-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.1.0" + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "@babel/plugin-transform-parameters": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.1.0.tgz", - "integrity": "sha512-vHV7oxkEJ8IHxTfRr3hNGzV446GAb+0hgbA7o/0Jd76s+YzccdWuTU296FOCOl/xweU4t/Ya4g41yWz80RFCRw==", + "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-call-delegate": "^7.1.0", - "@babel/helper-get-function-arity": "^7.0.0", - "@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.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.0.0.tgz", - "integrity": "sha512-BX8xKuQTO0HzINxT6j/GiCwoJB0AOMs0HmLbEnAvcte8U8rSkNa/eSCAY+l1OA4JnCVq2jw2p6U8QQryy2fTPg==", + "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.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.0.0.tgz", - "integrity": "sha512-0TMP21hXsSUjIQJmu/r7RiVxeFrXRcMUigbKu0BLegJK9PkYodHstaszcig7zxXfaBji2LYUdtqIkHs+hgYkJQ==", + "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.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0" + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "@babel/plugin-transform-react-jsx-self": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.0.0.tgz", - "integrity": "sha512-pymy+AK12WO4safW1HmBpwagUQRl9cevNX+82AIAtU1pIdugqcH+nuYP03Ja6B+N4gliAaKWAegIBL/ymALPHA==", + "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.0.0" - } + "license": "MIT" }, - "@babel/plugin-transform-react-jsx-source": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.0.0.tgz", - "integrity": "sha512-OSeEpFJEH5dw/TtxTg4nijl4nHBbhqbKL94Xo/Y17WKIf2qJWeIk/QeXACF19lG1vMezkxqruwnTjVizaW7u7w==", + "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.0.0" + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "@babel/plugin-transform-regenerator": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0.tgz", - "integrity": "sha512-sj2qzsEx8KDVv1QuJc/dEfilkg3RRPvPYx/VnKLtItVQRWt1Wqf5eVCOLZm29CiGFfYYsA3VPjfizTCV0S0Dlw==", + "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.13.3" + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.0.0.tgz", - "integrity": "sha512-g/99LI4vm5iOf5r1Gdxq5Xmu91zvjhEG5+yZDJW268AZELAu4J1EiFLnkSG3yuUsZyOipVOVUKoGPYwfsTymhw==", + "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-spread": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.0.0.tgz", - "integrity": "sha512-L702YFy2EvirrR4shTj0g2xQp7aNwZoWNCkNu2mcoU0uyzMl0XRwDSwzB/xp6DSUFiBmEXuyAyEN16LsgVqGGQ==", + "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-plugin-utils": "^7.0.0" + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" } }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.0.0.tgz", - "integrity": "sha512-LFUToxiyS/WD+XEWpkx/XJBrUXKewSZpzX68s+yEOtIbdnsRjpryDw9U06gYc6klYEij/+KQVRnD3nz3AoKmjw==", + "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", - "@babel/helper-regex": "^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-template-literals": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.0.0.tgz", - "integrity": "sha512-vA6rkTCabRZu7Nbl9DfLZE1imj4tzdWcg5vtdQGvj+OH9itNNB6hxuRMHuIY8SGnEt1T9g5foqs9LnrHzsqEFg==", + "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-annotate-as-pure": "^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-typeof-symbol": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.0.0.tgz", - "integrity": "sha512-1r1X5DO78WnaAIvs5uC48t41LLckxsYklJrZjNKcevyz83sF2l4RHbw29qrCPr/6ksFsdfRpT/ZgxNWHXRnffg==", + "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" + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.0.0.tgz", - "integrity": "sha512-uJBrJhBOEa3D033P95nPHu3nbFwFE9ZgXsfEitzoIXIwqAZWk7uXcg06yFKXz9FSxBH5ucgU/cYdX0IV8ldHKw==", + "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-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.0.0", - "regexpu-core": "^4.1.3" - } + "license": "MIT" }, - "@babel/preset-env": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.1.0.tgz", - "integrity": "sha512-ZLVSynfAoDHB/34A17/JCZbyrzbQj59QC1Anyueb4Bwjh373nVPq5/HMph0z+tCmcDjXDe+DlKQq9ywQuvWrQg==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-async-generator-functions": "^7.1.0", - "@babel/plugin-proposal-json-strings": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.0.0", - "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.0.0", - "@babel/plugin-syntax-async-generators": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.0.0", - "@babel/plugin-transform-arrow-functions": "^7.0.0", - "@babel/plugin-transform-async-to-generator": "^7.1.0", - "@babel/plugin-transform-block-scoped-functions": "^7.0.0", - "@babel/plugin-transform-block-scoping": "^7.0.0", - "@babel/plugin-transform-classes": "^7.1.0", - "@babel/plugin-transform-computed-properties": "^7.0.0", - "@babel/plugin-transform-destructuring": "^7.0.0", - "@babel/plugin-transform-dotall-regex": "^7.0.0", - "@babel/plugin-transform-duplicate-keys": "^7.0.0", - "@babel/plugin-transform-exponentiation-operator": "^7.1.0", - "@babel/plugin-transform-for-of": "^7.0.0", - "@babel/plugin-transform-function-name": "^7.1.0", - "@babel/plugin-transform-literals": "^7.0.0", - "@babel/plugin-transform-modules-amd": "^7.1.0", - "@babel/plugin-transform-modules-commonjs": "^7.1.0", - "@babel/plugin-transform-modules-systemjs": "^7.0.0", - "@babel/plugin-transform-modules-umd": "^7.1.0", - "@babel/plugin-transform-new-target": "^7.0.0", - "@babel/plugin-transform-object-super": "^7.1.0", - "@babel/plugin-transform-parameters": "^7.1.0", - "@babel/plugin-transform-regenerator": "^7.0.0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0", - "@babel/plugin-transform-spread": "^7.0.0", - "@babel/plugin-transform-sticky-regex": "^7.0.0", - "@babel/plugin-transform-template-literals": "^7.0.0", - "@babel/plugin-transform-typeof-symbol": "^7.0.0", - "@babel/plugin-transform-unicode-regex": "^7.0.0", - "browserslist": "^4.1.0", - "invariant": "^2.2.2", - "js-levenshtein": "^1.1.3", - "semver": "^5.3.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/@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", - "@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": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "@babel/runtime": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.1.2.tgz", - "integrity": "sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg==", + "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": { - "regenerator-runtime": "^0.12.0" - } + "license": "BSD-3-Clause" }, - "@babel/runtime-corejs2": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.1.2.tgz", - "integrity": "sha512-drxaPByExlcRDKW4ZLubUO4ZkI8/8ax9k9wve1aEthdLKFzjB7XRkOQ0xoTIWGxqdDnWDElkjYq77bt7yrcYJQ==", - "dev": true, - "requires": { - "core-js": "^2.5.7", - "regenerator-runtime": "^0.12.0" - } + "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/template": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.1.0.tgz", - "integrity": "sha512-yZ948B/pJrwWGY6VxG6XRFsVTee3IQ7bihq9zFpM00Vydu6z5Xwg0C3J644kxI9WOTzd+62xcIsQ+AT1MGhqhA==", + "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": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "license": "ISC", + "dependencies": { + "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/traverse": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.1.0.tgz", - "integrity": "sha512-bwgln0FsMoxm3pLOgrrnGaXk18sSM9JNf1/nHC/FksmNGFbYnPWY4GYCfLxyP1KRmfsxqkRpfoa6xr6VuuSxdw==", + "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/code-frame": "^7.0.0", - "@babel/generator": "^7.0.0", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.0.0", - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "debug": "^3.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.10" + "license": "MIT", + "engines": { + "node": ">=12" }, - "dependencies": { - "debug": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", - "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - } + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "@babel/types": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0.tgz", - "integrity": "sha512-5tPDap4bGKTLPtci2SUl/B7Gv8RnuJFuQoWx26RJobS0fFrz4reUA3JnwIM+HVHEmWE0C1mzKhDtTp8NsWY02Q==", + "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": { - "esutils": "^2.0.2", - "lodash": "^4.17.10", - "to-fast-properties": "^2.0.0" + "license": "MIT", + "engines": { + "node": ">=12" }, - "dependencies": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - } + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "@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/@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": { - "@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" - }, + "license": "MIT" + }, + "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, + "license": "MIT", "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 - } + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "@emotion/hash": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.6.6.tgz", - "integrity": "sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ==", - "dev": true - }, - "@emotion/memoize": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.6.tgz", - "integrity": "sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ==", - "dev": true - }, - "@emotion/serialize": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.9.1.tgz", - "integrity": "sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ==", + "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": { - "@emotion/hash": "^0.6.6", - "@emotion/memoize": "^0.6.6", - "@emotion/unitless": "^0.6.7", - "@emotion/utils": "^0.8.2" + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "@emotion/stylis": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.7.1.tgz", - "integrity": "sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ==", - "dev": true - }, - "@emotion/unitless": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.6.7.tgz", - "integrity": "sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg==", - "dev": true - }, - "@emotion/utils": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.8.2.tgz", - "integrity": "sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw==", - "dev": true - }, - "@gulp-sourcemaps/identity-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-1.0.1.tgz", - "integrity": "sha1-z6I7xYQPkQTOMqZedNt+epdLvuE=", + "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": { - "acorn": "^5.0.3", - "css": "^2.2.1", - "normalize-path": "^2.1.1", - "source-map": "^0.5.6", - "through2": "^2.0.3" + "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" } }, - "@gulp-sourcemaps/map-sources": { + "node_modules/@istanbuljs/load-nyc-config": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", - "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o=", + "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": { - "normalize-path": "^2.0.1", - "through2": "^2.0.3" - } - }, - "@jupyterlab/coreutils": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-2.1.4.tgz", - "integrity": "sha512-jSjn+xZj+kJrSWJ2Hz3jpxq0EXNpQrsnf+dD+pnDPc8mAeQ2XVu/x63VZyIDx8u2MELrIodFsUmQTxeudXKlOg==", - "requires": { - "@phosphor/algorithm": "^1.1.2", - "@phosphor/coreutils": "^1.3.0", - "@phosphor/disposable": "^1.1.2", - "@phosphor/signaling": "^1.2.2", - "ajv": "~5.1.6", - "comment-json": "^1.1.3", - "minimist": "~1.2.0", - "moment": "~2.21.0", - "path-posix": "~1.0.0", - "url-parse": "~1.4.3" - }, "dependencies": { - "ajv": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.1.6.tgz", - "integrity": "sha1-Sy8aGd7Ok9V6whYDfj6XkcfdFWQ=", - "requires": { - "co": "^4.6.0", - "json-schema-traverse": "^0.3.0", - "json-stable-stringify": "^1.0.1" - } - } + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" } }, - "@jupyterlab/observables": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@jupyterlab/observables/-/observables-2.0.7.tgz", - "integrity": "sha512-EbmQH7+9SOBZXq3wOhBII7DQXeOeu7v5v8qPK7hJrjGUWSZhGYGglzm05ou5bP/Q2h+Y+Ar3tYX/LUupR6JuaA==", - "requires": { - "@phosphor/algorithm": "^1.1.2", - "@phosphor/coreutils": "^1.3.0", - "@phosphor/disposable": "^1.1.2", - "@phosphor/messaging": "^1.2.2", - "@phosphor/signaling": "^1.2.2" + "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": { + "@istanbuljs/schema": "^0.1.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "nyc": ">=15" } }, - "@jupyterlab/services": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@jupyterlab/services/-/services-3.1.4.tgz", - "integrity": "sha512-EK8WDbqGWsbsYAtd1AbpelaXOrtuHDgV5fNcdXTnHyhvA/uXn/yYRI+mKO2sTjVjAjVL6n/XNomd0czzp+kiGg==", - "requires": { - "@jupyterlab/coreutils": "^2.1.4", - "@jupyterlab/observables": "^2.0.7", - "@phosphor/algorithm": "^1.1.2", - "@phosphor/coreutils": "^1.3.0", - "@phosphor/disposable": "^1.1.2", - "@phosphor/signaling": "^1.2.2", - "node-fetch": "~1.7.3", - "ws": "~1.1.4" + "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, + "engines": { + "node": ">=8" } }, - "@mapbox/polylabel": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mapbox/polylabel/-/polylabel-1.0.2.tgz", - "integrity": "sha1-xXFGGbZa3QgmOOoGAn5psUUA76Y=", + "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": { - "tinyqueue": "^1.1.0" + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" } }, - "@nteract/markdown": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@nteract/markdown/-/markdown-2.1.4.tgz", - "integrity": "sha512-nRJuAfX+3n/geJAQj6IqEsbhpiPOjyFlUTzh1bdT2hyLGbd2VdbANXDliWTDKVHOHYWxh0zOLQhRvQ4B6G5QlQ==", + "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": { - "@babel/runtime-corejs2": "^7.0.0", - "@nteract/mathjax": "^2.1.4", - "babel-runtime": "^6.26.0", - "prop-types": "^15.6.1", - "react-markdown": "^3.1.4" + "engines": { + "node": ">=6.0.0" } }, - "@nteract/mathjax": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@nteract/mathjax/-/mathjax-2.1.4.tgz", - "integrity": "sha512-dY+h3iBsfioSg+33uB04LE9tIt79ClbEumXKz7eciS251jXw8VZcmo/YFqldThpgckbCvveNCliM4Y5sTk1gog==", + "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": { - "@babel/runtime-corejs2": "^7.0.0", - "babel-runtime": "^6.26.0", - "prop-types": "^15.6.1" + "engines": { + "node": ">=6.0.0" } }, - "@nteract/octicons": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@nteract/octicons/-/octicons-0.4.3.tgz", - "integrity": "sha512-spBTHmaD4+W/Ww0UQt1uXmeYGM5GBA5TExdcuDBn3dLWqsfwjf1nTEfygLx4TRtuqImfPAuUHM4iV+2WkXgMBg==", + "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": { - "@babel/runtime-corejs2": "^7.0.0", - "babel-runtime": "^6.26.0" + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, - "@nteract/plotly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nteract/plotly/-/plotly-1.0.0.tgz", - "integrity": "sha1-HORUqxuqZ9wm5y/1qRfCBl7tkUY=", + "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 }, - "@nteract/transform-dataresource": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@nteract/transform-dataresource/-/transform-dataresource-4.3.5.tgz", - "integrity": "sha512-lPLmZJSiTn6x3zo3zQM+xgY6XXAg2ocrqhQb+zczfPPlINs9TeOmT39R2+QKO9VycOhPIGlAh1n/IHRQOAE6eQ==", + "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": { - "@babel/runtime": "^7.0.0", - "@babel/runtime-corejs2": "^7.0.0", - "@nteract/octicons": "^0.4.3", - "@nteract/transform-plotly": "^3.2.3", - "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.1", - "semiotic": "^1.14.4", - "tv4": "^1.3.0" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "@nteract/transform-geojson": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@nteract/transform-geojson/-/transform-geojson-3.2.3.tgz", - "integrity": "sha512-7LDEUik1DNr11ajp66LqfEnP+CYi0XtaUVJMKN4U+YyGCYcLqtHi9OkLF6jjGe/6SSyL+ukzVU/LNSRJRMN+dA==", - "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "babel-runtime": "^6.26.0", - "leaflet": "^1.0.3" + "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" } }, - "@nteract/transform-model-debug": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@nteract/transform-model-debug/-/transform-model-debug-3.2.3.tgz", - "integrity": "sha512-TVNUtTbc0W3cgbdUMFpTqiCWziMpI/7zsR44bwoFScNFmf//HjsQtcXPwj418AHLmywblIeMbtiLFDhrpbpfww==", - "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "babel-runtime": "^6.26.0" + "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" } }, - "@nteract/transform-plotly": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@nteract/transform-plotly/-/transform-plotly-3.2.3.tgz", - "integrity": "sha512-JPBwW39glHcBF6swMIGWERw8+kcVIonoADPhrnseQcyScjx69Kd7tkAy58iCnVrV4fFvOUT1W0lGXSNTkMAh0Q==", - "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "@nteract/plotly": "^1.0.0", - "babel-runtime": "^6.26.0", - "lodash": "^4.17.4" + "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": "*" } }, - "@nteract/transform-vdom": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@nteract/transform-vdom/-/transform-vdom-2.2.3.tgz", - "integrity": "sha512-NIi4hZzmlXeisZoWU77z++hMGYabunNmDj/wNLAmOCAGnGH6rD3ZTszrBOkP2pPlnm0Je3OqJ2Pun3GZKdCOpQ==", - "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "babel-runtime": "^6.26.0" + "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": "*" } }, - "@nteract/transforms": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@nteract/transforms/-/transforms-4.4.4.tgz", - "integrity": "sha512-Y18j197/Dgz9KMOT+G3ocWQTRZcVVPQnMs6AbUgOSUTvoSiPMZb8ZQtIURRXS/1E1oAg05Ch6lVXlbnyClj4Ag==", - "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "@nteract/markdown": "^2.1.4", - "@nteract/mathjax": "^2.1.4", - "@nteract/transform-vdom": "^2.2.3", - "ansi-to-react": "^3.3.3", - "babel-runtime": "^6.26.0", - "prop-types": "^15.6.1", - "react-json-tree": "^0.11.0" + "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": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, - "@phosphor/algorithm": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.1.2.tgz", - "integrity": "sha1-/R3pEEyafzTpKGRYbd8ufy53eeg=" - }, - "@phosphor/collections": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@phosphor/collections/-/collections-1.1.2.tgz", - "integrity": "sha1-xMC4uREpkF+zap8kPy273kYtq40=", - "requires": { - "@phosphor/algorithm": "^1.1.2" + "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": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" } }, - "@phosphor/coreutils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@phosphor/coreutils/-/coreutils-1.3.0.tgz", - "integrity": "sha1-YyktOBwBLFqw0Blug87YKbfgSkI=" + "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": "*" + } }, - "@phosphor/disposable": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.1.2.tgz", - "integrity": "sha1-oZLdai5sadXQnTns8zTauTd4Bg4=", - "requires": { - "@phosphor/algorithm": "^1.1.2" + "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": { + "@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": "*" } }, - "@phosphor/messaging": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@phosphor/messaging/-/messaging-1.2.2.tgz", - "integrity": "sha1-fYlt3TeXuUo0dwje0T2leD23XBQ=", - "requires": { - "@phosphor/algorithm": "^1.1.2", - "@phosphor/collections": "^1.1.2" + "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" } }, - "@phosphor/signaling": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.2.2.tgz", - "integrity": "sha1-P8+Xyojji/s1f+j+a/dRM0elFKk=", - "requires": { - "@phosphor/algorithm": "^1.1.2" + "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" } }, - "@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 + "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": "*" + } }, - "@types/anymatch": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.0.tgz", - "integrity": "sha512-7WcbyctkE8GTzogDb0ulRAEw7v8oIS54ft9mQTU7PfM0hp5e+8kpa+HeQ7IQrFbKtJXBKcZ4bh+Em9dTw5L6AQ==", - "dev": true + "node_modules/@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==" }, - "@types/caseless": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", - "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==", - "dev": true + "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": "*" + } }, - "@types/chai": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.3.tgz", - "integrity": "sha512-f5dXGzOJycyzSMdaXVhiBhauL4dYydXwVpavfQ1mVCaGjR56a9QfklXObUxlIY9bGTmCPHEEZ04I16BZ/8w5ww==", - "dev": true + "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": "*" + } }, - "@types/chai-arrays": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/chai-arrays/-/chai-arrays-1.0.2.tgz", - "integrity": "sha512-/kgYvj5Pwiv/bOlJ6c5GlRF/W6lUGSLrpQGl/7Gg6w7tvBYcf0iF91+wwyuwDYGO2zM0wNpcoPixZVif8I/r6g==", - "dev": true, - "requires": { - "@types/chai": "*" + "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" } }, - "@types/chai-as-promised": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.0.tgz", - "integrity": "sha512-MFiW54UOSt+f2bRw8J7LgQeIvE/9b4oGvwU7XW30S9QGAiHGnU/fmiOprsyMkdmH2rl8xSPc0/yrQw8juXU6bQ==", - "dev": true, - "requires": { - "@types/chai": "*" + "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" } }, - "@types/cheerio": { - "version": "0.22.9", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.9.tgz", - "integrity": "sha512-q6LuBI0t5u04f0Q4/R+cGBqIbZMtJkVvCSF+nTfFBBdQqQvJR/mNHeWjRkszyLl7oyf2rDoKUYMEjTw5AV0hiw==", - "dev": true + "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==" }, - "@types/clean-css": { - "version": "3.4.30", - "resolved": "http://registry.npmjs.org/@types/clean-css/-/clean-css-3.4.30.tgz", - "integrity": "sha1-AFLBNvUkgAJCjjY4s33ko5gYZB0=", - "dev": true + "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==" }, - "@types/commander": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz", - "integrity": "sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==", - "dev": true, - "requires": { - "commander": "*" + "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" } }, - "@types/copy-webpack-plugin": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@types/copy-webpack-plugin/-/copy-webpack-plugin-4.4.2.tgz", - "integrity": "sha512-/L0m5kc7pKGpsu97TTgAP6YcVRmau2Wj0HpRPQBGEbZXT1DZkdozZPCZHGDWXpxcvWDFTxob2JmYJj3RC7CwFA==", + "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, - "requires": { - "@types/minimatch": "*", - "@types/webpack": "*" + "bin": { + "semver": "bin/semver.js" } }, - "@types/decompress": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.2.tgz", - "integrity": "sha512-2jlSsNAVhrWJtgOV3V85MJ09yRoeUTUWQeeusNYAcJVkUmoVRVElvmkWN0TK+Lgdlyd9pIRyja/DTBcyqD8xyA==", + "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": { - "@types/node": "*" + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" } }, - "@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/@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, - "requires": { - "@types/glob": "*" + "engines": { + "node": ">= 8" } }, - "@types/dotenv": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-4.0.3.tgz", - "integrity": "sha512-mmhpINC/HcLGQK5ikFJlLXINVvcxhlrV+ZOUJSN7/ottYl+8X4oSXzS9lBtDkmWAl96EGyGyLrNvk9zqdSH8Fw==", + "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": { - "@types/node": "*" + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" } }, - "@types/download": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@types/download/-/download-6.2.2.tgz", - "integrity": "sha512-gwRnrp1yFweJhPGBR01nfesxYcml8SayxHEwA6x+1T+Lqez5iMdCRJgt/I9HqpjMi5Mmtb/7MswY6FN4bMypNg==", - "dev": true, - "requires": { - "@types/decompress": "*", - "@types/got": "*", - "@types/node": "*" + "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" } }, - "@types/enzyme": { - "version": "3.1.14", - "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.1.14.tgz", - "integrity": "sha512-jvAbagrpoSNAXeZw2kRpP10eTsSIH8vW1IBLCXbN0pbZsYZU8FvTPMMd5OzSWUKWTQfrbXFUY8e6un/W4NpqIA==", - "dev": true, - "requires": { - "@types/cheerio": "*", - "@types/react": "*" + "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": { + "@opentelemetry/semantic-conventions": "1.15.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.5.0" } }, - "@types/enzyme-adapter-react-16": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.3.tgz", - "integrity": "sha512-9eRLBsC/Djkys05BdTWgav8v6fSCjyzjNuLwG2sfa2b2g/VAN10luP0zB0VwtOWFQ0LGjIboJJvIsVdU5gqRmg==", - "dev": true, - "requires": { - "@types/enzyme": "*" + "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" } }, - "@types/event-stream": { - "version": "3.3.34", - "resolved": "https://registry.npmjs.org/@types/event-stream/-/event-stream-3.3.34.tgz", - "integrity": "sha512-LLiivgWKii4JeMzFy3trrxqkRrVSdue8WmbXyHuSJLwNrhIQU5MTrc65jhxEPwMyh5HR1xevSdD+k2nnSRKw9g==", - "dev": true, - "requires": { - "@types/node": "*" + "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" } }, - "@types/events": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", - "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", - "dev": true + "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" + } }, - "@types/form-data": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", - "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", - "dev": true, - "requires": { - "@types/node": "*" + "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" } }, - "@types/fs-extra": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.0.2.tgz", - "integrity": "sha512-Q3FWsbdmkQd1ib11A4XNWQvRD//5KpPoGawA8aB2DR7pWKoW9XQv3+dGxD/Z1eVFze23Okdo27ZQytVFlweKvQ==", + "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": { - "@types/node": "*" + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" } }, - "@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/@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 }, - "@types/glob": { - "version": "5.0.35", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-5.0.35.tgz", - "integrity": "sha512-wc+VveszMLyMWFvXLkloixT4n0harUIVZjnpzztaZ0nKLuul7Z32iMt2fUFGAaZ4y1XWjFRMtCI5ewvyh4aIeg==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/got": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@types/got/-/got-8.3.1.tgz", - "integrity": "sha512-CGEPw67/Ub6gNMusk062tueurxN+HyjDCvYl4QVBKiSO+fqluXmRX/wSqST/4RtKth4mz8lDZiaZIpXr/uPROg==", + "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": { - "@types/node": "*" - } + "license": "MIT" }, - "@types/html-minifier": { - "version": "3.5.2", - "resolved": "http://registry.npmjs.org/@types/html-minifier/-/html-minifier-3.5.2.tgz", - "integrity": "sha512-yikK28/KlVyf8g9i/k+TDFlteLuZ6QQTUdVqvKtzEB+8DSLCTjxfh6IK45KnW4rYFI3Y8T4LWpYJMTmfJleWaQ==", + "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, - "requires": { - "@types/clean-css": "*", - "@types/relateurl": "*", - "@types/uglify-js": "*" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "@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/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, - "requires": { - "@types/html-minifier": "*", - "@types/tapable": "*", - "@types/webpack": "*" + "dependencies": { + "type-detect": "4.0.8" } }, - "@types/iconv-lite": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@types/iconv-lite/-/iconv-lite-0.0.1.tgz", - "integrity": "sha1-qjuL2ivlErGuCgV7lC6GnDcKVWk=", + "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, - "requires": { - "@types/node": "*" + "dependencies": { + "@sinonjs/commons": "^3.0.0" } }, - "@types/istanbul": { - "version": "0.4.30", - "resolved": "https://registry.npmjs.org/@types/istanbul/-/istanbul-0.4.30.tgz", - "integrity": "sha512-+hQU4fh2G96ze78uI5/V6+SRDZD1UnVrFn23i2eDetwfbBq3s0/zYP92xj/3qyvVMM3WnvS88N56zjz+HmL04A==", - "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/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", "dev": true, - "requires": { - "@types/events": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^4.0.0" - }, "dependencies": { - "parse5": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", - "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", - "dev": true - } + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" } }, - "@types/json5": { - "version": "0.0.29", - "resolved": "http://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true - }, - "@types/loader-utils": { - "version": "1.1.3", - "resolved": "http://registry.npmjs.org/@types/loader-utils/-/loader-utils-1.1.3.tgz", - "integrity": "sha512-euKGFr2oCB3ASBwG39CYJMR3N9T0nanVqXdiH7Zu/Nqddt6SmFRxytq/i2w9LQYNQekEtGBz+pE3qG6fQTNvRg==", + "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, - "requires": { - "@types/node": "*", - "@types/webpack": "*" + "dependencies": { + "type-detect": "4.0.8" } }, - "@types/lodash": { - "version": "4.14.109", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.109.tgz", - "integrity": "sha512-hop8SdPUEzbcJm6aTsmuwjIYQo1tqLseKCM+s2bBqTU2gErwI4fE+aqUVOlscPSQbKHKgtMMPoC+h4AIGOJYvw==", + "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 }, - "@types/md5": { - "version": "2.1.32", - "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.1.32.tgz", - "integrity": "sha1-k+I0N/zRenucqY0CqmAC6DWEL+g=", + "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": { - "@types/node": "*" + "engines": { + "node": ">= 6" } }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/mocha": { - "version": "2.2.48", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.48.tgz", - "integrity": "sha512-nlK/iyETgafGli8Zh9zJVCTicvU3iajSkRwOh3Hhiva598CMqNJ4NcVCGMTGKpGpTYj/9R8RLzS9NAykSSCqGw==", - "dev": true - }, - "@types/node": { - "version": "9.4.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-9.4.7.tgz", - "integrity": "sha512-4Ba90mWNx8ddbafuyGGwjkZMigi+AWfYLSDCpovwsE63ia8w93r3oJ8PIAQc3y8U+XHcnMOHPIzNe3o438Ywcw==", + "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 }, - "@types/prismjs": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.9.0.tgz", - "integrity": "sha512-zeh+xd2pcCvWm1XtWLR4v5pzZMybKeq6X8Q4cIZMMx8GmyKDUfJaOtw+JaONHUQt5ncKFXezl8QGIDQsSF5YfA==", + "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 }, - "@types/promisify-node": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@types/promisify-node/-/promisify-node-0.4.0.tgz", - "integrity": "sha1-3MceY8Cr9oYbrn0S9swzlceDk2s=", + "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 }, - "@types/prop-types": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.6.tgz", - "integrity": "sha512-ZBFR7TROLVzCkswA3Fmqq+IIJt62/T7aY/Dmz+QkU7CaW2QFqAitCE8Ups7IzmGhcN1YWMBT4Qcoc07jU9hOJQ==", + "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 }, - "@types/react": { - "version": "16.4.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.4.14.tgz", - "integrity": "sha512-Gh8irag2dbZ2K6vPn+S8+LNrULuG3zlCgJjVUrvuiUK7waw9d9CFk2A/tZFyGhcMDUyO7tznbx1ZasqlAGjHxA==", + "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, - "requires": { - "@types/prop-types": "*", - "csstype": "^2.2.0" + "dependencies": { + "@types/node": "*" } }, - "@types/react-dom": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.0.8.tgz", - "integrity": "sha512-WF/KAOia7pskV+J8f+UlNuFeCRkJuJAkyyeYPPtNe6suw0y7cWyUP/DPdPXsGUwQEkv2qlLVSrgVaoCm/PmO0Q==", + "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, - "requires": { - "@types/node": "*", - "@types/react": "*" + "dependencies": { + "@types/chai": "*" } }, - "@types/react-json-tree": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/@types/react-json-tree/-/react-json-tree-0.6.8.tgz", - "integrity": "sha512-OyCFv5pHZXVULzjbNXBz+Il+vcYz8RzHl1BXQ297XMBTu4+oqVdZUVgU/PMmndSO05met1KqtKVJaj2K5K79+g==", + "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": { - "@types/react": "*" + "dependencies": { + "@types/chai": "*" } }, - "@types/relateurl": { - "version": "0.2.28", - "resolved": "http://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.28.tgz", - "integrity": "sha1-a9p9uGU/piZD9e5p6facEaOS46Y=", + "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 }, - "@types/request": { - "version": "2.47.0", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.47.0.tgz", - "integrity": "sha512-/KXM5oev+nNCLIgBjkwbk8VqxmzI56woD4VUxn95O+YeQ8hJzcSmIZ1IN3WexiqBb6srzDo2bdMbsXxgXNkz5Q==", + "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, - "requires": { - "@types/caseless": "*", - "@types/form-data": "*", - "@types/node": "*", - "@types/tough-cookie": "*" + "dependencies": { + "@types/node": "*" } }, - "@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/download": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@types/download/-/download-8.0.3.tgz", + "integrity": "sha512-IDwXjU7zCtuFVvI0Plnb02TpXyj3RA4YeOKQvEfsjdJeWxZ9hTl6lxeNsU2bLWn0aeAS7fyMl74w/TbdOlS2KQ==", + "dev": true, + "dependencies": { + "@types/decompress": "*", + "@types/got": "^9", + "@types/node": "*" + } }, - "@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/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": { + "@types/estree": "*", + "@types/json-schema": "*" + } }, - "@types/sinon": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.3.tgz", - "integrity": "sha512-Tt7w/ylBS/OEAlSCwzB0Db1KbxnkycP/1UkQpbvKFYoUuRn4uYsC3xh5TRPrOjTy0i8TIkSz1JdNL4GPVdf3KQ==", - "dev": true + "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, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } }, - "@types/tapable": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.4.tgz", - "integrity": "sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==", + "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 }, - "@types/temp": { - "version": "0.8.32", - "resolved": "https://registry.npmjs.org/@types/temp/-/temp-0.8.32.tgz", - "integrity": "sha512-gyIhOlWPqI8vtYTlRb61HKV7x+3wjpJIQi8mTaweVtEMvhIV6Xajo8FVcNJWeJOBuedRCzK2Uy+uhj/rJmR9oQ==", + "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": { + "dependencies": { + "@types/jsonfile": "*", "@types/node": "*" } }, - "@types/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", - "dev": true - }, - "@types/tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha512-MDQLxNFRLasqS4UlkWMSACMKeSm1x4Q3TxzUC7KQUsh6RK1ZrQ0VEyE3yzXcBu+K8ejVj4wuX32eUG02yNp+YQ==", - "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/@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": { - "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 - } + "@types/minimatch": "*", + "@types/node": "*" } }, - "@types/untildify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/untildify/-/untildify-3.0.0.tgz", - "integrity": "sha512-FTktI3Y1h+gP9GTjTvXBP5v8xpH4RU6uS9POoBcGy4XkS2Np6LNtnP1eiNNth4S7P+qw2c/rugkwBasSHFzJEg==", - "dev": true - }, - "@types/uuid": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.3.tgz", - "integrity": "sha512-5fRLCYhLtDb3hMWqQyH10qtF+Ud2JnNCXTCZ+9ktNdCcgslcuXkDTkFcJNk++MT29yDntDnlF1+jD+uVGumsbw==", + "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": { - "@types/node": "*" + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" } }, - "@types/webpack": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.4.19.tgz", - "integrity": "sha512-vO/PuQ9iF9Gy8spN8RUUjt5reu9Z+Tb7iWxeAopCmXaIZaIsOgtY5U6UE2ELlcRUBO1HbNWhy+lQE9G92IJcmQ==", + "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": { - "@types/anymatch": "*", - "@types/node": "*", - "@types/tapable": "*", - "@types/uglify-js": "*", - "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 - } + "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" } }, - "@types/webpack-bundle-analyzer": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.13.0.tgz", - "integrity": "sha512-+qy5xatScNZW4NbIVaiV38XOeHbKRa4FIPeMf2VDpZEon9W/cxjaVR080vRrRGvfq4tRvOusTEypSMxTvjcSzw==", + "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": { - "@types/webpack": "*" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "@types/winreg": { - "version": "1.2.30", - "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.30.tgz", - "integrity": "sha1-kdZxDlNtNFucmwF8V0z2qNpkxRg=", + "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 }, - "@types/xml2js": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.2.tgz", - "integrity": "sha512-8aKUBSj3oGcnuiBmDLm3BIk09RYg01mz9HlQ2u4aS17oJ25DxjQrEUVGFSBVNOfM45pQW4OjcBPplq6r/exJdA==", + "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 + }, + "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": { + "dependencies": { "@types/node": "*" } }, - "@webassemblyjs/ast": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.8.tgz", - "integrity": "sha512-dOrtdtEyB8sInpl75yLPNksY4sRl0j/+t6aHyB/YA+ab9hV3Fo7FmG12FHzP+2MvWVAJtDb+6eXR5EZbZJ+uVg==", + "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": { - "@webassemblyjs/helper-module-context": "1.7.8", - "@webassemblyjs/helper-wasm-bytecode": "1.7.8", - "@webassemblyjs/wast-parser": "1.7.8" + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" } }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.8.tgz", - "integrity": "sha512-kn2zNKGsbql5i56VAgRYkpG+VazqHhQQZQycT2uXAazrAEDs23gy+Odkh5VblybjnwX2/BITkDtNmSO76hdIvQ==", + "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 }, - "@webassemblyjs/helper-api-error": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.8.tgz", - "integrity": "sha512-xUwxDXsd1dUKArJEP5wWM5zxgCSwZApSOJyP1XO7M8rNUChUDblcLQ4FpzTpWG2YeylMwMl1MlP5Ztryiz1x4g==", - "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==" }, - "@webassemblyjs/helper-buffer": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.8.tgz", - "integrity": "sha512-WXiIMnuvuwlhWvVOm8xEXU9DnHaa3AgAU0ZPfvY8vO1cSsmYb2WbGbHnMLgs43vXnA7XAob9b56zuZaMkxpCBg==", + "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 }, - "@webassemblyjs/helper-code-frame": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.8.tgz", - "integrity": "sha512-TLQxyD9qGOIdX5LPQOPo0Ernd88U5rHkFb8WAjeMIeA0sPjCHeVPaGqUGGIXjUcblUkjuDAc07bruCcNHUrHDA==", + "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": { - "@webassemblyjs/wast-printer": "1.7.8" + "dependencies": { + "@types/sinonjs__fake-timers": "*" } }, - "@webassemblyjs/helper-fsm": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.8.tgz", - "integrity": "sha512-TjK0CnD8hAPkV5mbSp5aWl6SO1+H3WFcjWtixWoy8EMA99YnNzYhpc/WSYWhf7yrhpzkq5tZB0tvLK3Svr3IXA==", + "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 }, - "@webassemblyjs/helper-module-context": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.8.tgz", - "integrity": "sha512-uCutAKR7Nm0VsFixcvnB4HhAyHouNbj0Dx1p7eRjFjXGGZ+N7ftTaG1ZbWCasAEbtwGj54LP8+lkBZdTCPmLGg==", + "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 }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.8.tgz", - "integrity": "sha512-AdCCE3BMW6V34WYaKUmPgVHa88t2Z14P4/0LjLwuGkI0X6pf7nzp0CehzVVk51cKm2ymVXjl9dCG+gR1yhITIQ==", + "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 }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.8.tgz", - "integrity": "sha512-BkBhYQuzyl4hgTGOKo87Vdw6f9nj8HhI7WYpI0MCC5qFa5ahrAPOGgyETVdnRbv+Rjukl9MxxfDmVcVC435lDg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.8", - "@webassemblyjs/helper-buffer": "1.7.8", - "@webassemblyjs/helper-wasm-bytecode": "1.7.8", - "@webassemblyjs/wasm-gen": "1.7.8" - } + "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 }, - "@webassemblyjs/ieee754": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.7.8.tgz", - "integrity": "sha512-tOarWChdG1a3y1yqCX0JMDKzrat5tQe4pV6K/TX19BcXsBLYxFQOL1DEDa5KG9syeyvCrvZ+i1+Mv1ExngvktQ==", + "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": { - "@xtuc/ieee754": "^1.2.0" - } + "license": "MIT" }, - "@webassemblyjs/leb128": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.7.8.tgz", - "integrity": "sha512-GCYeGPgUFWJiZuP4NICbcyUQNxNLJIf476Ei+K+jVuuebtLpfvwkvYT6iTUE7oZYehhkor4Zz2g7SJ/iZaPudQ==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.1" - } + "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 }, - "@webassemblyjs/utf8": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.7.8.tgz", - "integrity": "sha512-9X+f0VV+xNXW2ujfIRSXBJENGE6Qh7bNVKqu3yDjTFB3ar3nsThsGBBKdTG58aXOm2iUH6v28VIf88ymPXODHA==", + "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 }, - "@webassemblyjs/wasm-edit": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.8.tgz", - "integrity": "sha512-6D3Hm2gFixrfyx9XjSON4ml1FZTugqpkIz5Awvrou8fnpyprVzcm4X8pyGRtA2Piixjl3DqmX/HB1xdWyE097A==", + "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": { - "@webassemblyjs/ast": "1.7.8", - "@webassemblyjs/helper-buffer": "1.7.8", - "@webassemblyjs/helper-wasm-bytecode": "1.7.8", - "@webassemblyjs/helper-wasm-section": "1.7.8", - "@webassemblyjs/wasm-gen": "1.7.8", - "@webassemblyjs/wasm-opt": "1.7.8", - "@webassemblyjs/wasm-parser": "1.7.8", - "@webassemblyjs/wast-printer": "1.7.8" + "dependencies": { + "@types/node": "*" } }, - "@webassemblyjs/wasm-gen": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.8.tgz", - "integrity": "sha512-a7O/wE6eBeVKKUYgpMK7NOHmMADD85rSXLe3CqrWRDwWff5y3cSVbzpN6Qv3z6C4hdkpq9qyij1Ga1kemOZGvQ==", + "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": { - "@webassemblyjs/ast": "1.7.8", - "@webassemblyjs/helper-wasm-bytecode": "1.7.8", - "@webassemblyjs/ieee754": "1.7.8", - "@webassemblyjs/leb128": "1.7.8", - "@webassemblyjs/utf8": "1.7.8" + "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 + } } }, - "@webassemblyjs/wasm-opt": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.8.tgz", - "integrity": "sha512-3lbQ0PT81NHCdi1sR/7+SNpZadM4qYcTSr62nFFAA7e5lFwJr14M1Gi+A/Y3PgcDWOHYjsaNGPpPU0H03N6Blg==", + "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, - "requires": { - "@webassemblyjs/ast": "1.7.8", - "@webassemblyjs/helper-buffer": "1.7.8", - "@webassemblyjs/wasm-gen": "1.7.8", - "@webassemblyjs/wasm-parser": "1.7.8" + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "@webassemblyjs/wasm-parser": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.8.tgz", - "integrity": "sha512-rZ/zlhp9DHR/05zh1MbAjT2t624sjrPP/OkJCjXqzm7ynH+nIdNcn9Ixc+qzPMFXhIrk0rBoQ3to6sEIvHh9jQ==", + "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": { - "@webassemblyjs/ast": "1.7.8", - "@webassemblyjs/helper-api-error": "1.7.8", - "@webassemblyjs/helper-wasm-bytecode": "1.7.8", - "@webassemblyjs/ieee754": "1.7.8", - "@webassemblyjs/leb128": "1.7.8", - "@webassemblyjs/utf8": "1.7.8" + "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 + } } }, - "@webassemblyjs/wast-parser": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.7.8.tgz", - "integrity": "sha512-Q/zrvtUvzWuSiJMcSp90fi6gp2nraiHXjTV2VgAluVdVapM4gy1MQn7akja2p6eSBDQpKJPJ6P4TxRkghRS5dg==", + "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": { - "@webassemblyjs/ast": "1.7.8", - "@webassemblyjs/floating-point-hex-parser": "1.7.8", - "@webassemblyjs/helper-api-error": "1.7.8", - "@webassemblyjs/helper-code-frame": "1.7.8", - "@webassemblyjs/helper-fsm": "1.7.8", - "@xtuc/long": "4.2.1" + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "@webassemblyjs/wast-printer": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.7.8.tgz", - "integrity": "sha512-GllIthRtwTxRDAURRNXscu7Napzmdf1jt1gpiZiK/QN4fH0lSGs3OTmvdfsMNP7tqI4B3ZtfaaWRlNIQug6Xyg==", + "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": { - "@webassemblyjs/ast": "1.7.8", - "@webassemblyjs/wast-parser": "1.7.8", - "@xtuc/long": "4.2.1" + "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" } }, - "@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.1", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.1.tgz", - "integrity": "sha512-FZdkNBDqBRHKQ2MEbSC17xnPFOhZxeJ2YGSfr2BKf3sujG49Qe3bB+rGCwQfIaA7WHnGeGkSijX4FuBCdrzW/g==", - "dev": true - }, - "JSONStream": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.3.tgz", - "integrity": "sha512-3Sp6WZZ/lXl+nTDoGpGWHEpTnnC6X5fnkolYZR6nwIfzbxxvA8utPWe1gCt7i0m9uVGsSz2IS8K8mJ7HmlduMg==", + "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, - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" + "dependencies": { + "@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 + } } }, - "abab": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", - "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "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, - "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "acorn": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", - "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==", - "dev": true - }, - "acorn-dynamic-import": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz", - "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==", + "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": { - "acorn": "^5.0.0" + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "acorn-globals": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.0.tgz", - "integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==", + "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, - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - }, "dependencies": { - "acorn": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.2.tgz", - "integrity": "sha512-GXmKIvbrN3TV7aVqAzVFaMW8F8wzVX7voEBRO3bDA64+EX37YSayggRJP5Xig6HYHBkWKpFg9W5gg6orklubhg==", - "dev": true + "@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 } } }, - "acorn-walk": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.0.tgz", - "integrity": "sha512-ugTb7Lq7u4GfWSqqpwE0bGyoBZNMTok/zDBXxfEG0QM50jNlGhIWjRC1pPN7bvV1anhF+bs+/gNcRw+o55Evbg==", - "dev": true - }, - "address": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/address/-/address-1.0.3.tgz", - "integrity": "sha512-z55ocwKBRLryBs394Sm3ushTtBeg6VAeuku7utSoSnsJKvKcnXFIyC6vh27n3rXyxSgkJBBCAvyOn7gSUcTYjg==", - "dev": true - }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" + "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" } }, - "ajv-errors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.0.tgz", - "integrity": "sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk=", - "dev": true - }, - "ajv-keywords": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", - "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", - "dev": true - }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "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": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.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" - } + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } } }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "anser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.7.tgz", - "integrity": "sha512-0jA836gkgorW5M+yralEdnAuQ4Z8o/jAu9Po3//dAClUyq9LdKEIAVVZNoej9jfnRi20wPL/gBb3eTjpzppjLg==", - "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/@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, - "requires": { - "ansi-wrap": "^0.1.0" + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "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/@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": { - "ansi-wrap": "0.1.0" + "dependencies": { + "@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" } }, - "ansi-escapes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", - "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", + "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 }, - "ansi-gray": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", + "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": { - "ansi-wrap": "0.1.0" + "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" } }, - "ansi-red": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", - "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", + "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": { - "ansi-wrap": "0.1.0" + "license": "ISC" + }, + "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" } }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "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, + "dependencies": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "jszip": "^3.10.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=16" + } }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "ansi-to-html": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.6.7.tgz", - "integrity": "sha512-ma1GhrnEsR70TvGKneM6Fa1UCB76ZTuVjw9KiO/BSBaJfLFjvoiKC+i2BPBqkRoQaLl35I0Vi2g52XmKEKM7VA==", + "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": { - "entities": "^1.1.1" + "dependencies": { + "@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" } }, - "ansi-to-react": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ansi-to-react/-/ansi-to-react-3.3.3.tgz", - "integrity": "sha512-Ztq11TxaO157sv5rcVNa+jlPbGZxNtfslxWv5N5mDzyUWDZtwfiQAul/gegIOh5xTjN/FOoc2X6KBBnFsGbZ+g==", + "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-corejs2": "^7.0.0", - "anser": "^1.4.1", - "babel-runtime": "^6.26.0", - "escape-carriage": "^1.2.0" - } - }, - "ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", - "dev": true + "optional": true, + "os": [ + "alpine" + ] }, - "anymatch": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "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, - "requires": { - "micromatch": "^2.1.5", - "normalize-path": "^2.0.0" - }, - "dependencies": { - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1" - } - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, - "requires": { - "expand-range": "^1.8.1", - "preserve": "^0.2.0", - "repeat-element": "^1.1.2" - } - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "^0.1.0" - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "^1.0.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" - } - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "^2.0.0", - "array-unique": "^0.2.1", - "braces": "^1.8.2", - "expand-brackets": "^0.1.4", - "extglob": "^0.3.1", - "filename-regex": "^2.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.1", - "kind-of": "^3.0.2", - "normalize-path": "^2.0.1", - "object.omit": "^2.0.0", - "parse-glob": "^3.0.4", - "regex-cache": "^0.4.2" - } - } - } + "optional": true, + "os": [ + "alpine" + ] }, - "append-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", - "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", + "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, - "requires": { - "buffer-equal": "^1.0.0" - } - }, - "applicationinsights": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.0.6.tgz", - "integrity": "sha512-VQT3kBpJVPw5fCO5n+WUeSx0VHjxFtD7znYbILBlVgOS9/cMDuGFmV2Br3ObzFyZUDGNbEfW36fD1y2/vAiCKw==", - "requires": { - "diagnostic-channel": "0.2.0", - "diagnostic-channel-publishers": "0.2.1", - "zone.js": "0.7.6" - } + "optional": true, + "os": [ + "darwin" + ] }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "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" + ] }, - "arch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.0.tgz", - "integrity": "sha1-NhOqRhSQZLPB8GB5Gb8dR4boKIk=" + "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, + "optional": true, + "os": [ + "linux" + ] }, - "archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", - "dev": true + "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, + "optional": true, + "os": [ + "linux" + ] }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "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, - "requires": { - "sprintf-js": "~1.0.2" - } + "optional": true, + "os": [ + "linux" + ] }, - "argv": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", - "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", - "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" + ] }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + "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, + "optional": true, + "os": [ + "win32" + ] }, - "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/@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, - "requires": { - "make-iterator": "^1.0.0" + "engines": { + "node": ">= 6" } }, - "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==" + "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, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } }, - "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/@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": { - "make-iterator": "^1.0.0" + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "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/@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, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } }, - "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/@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 }, - "array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "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 }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "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 }, - "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 + "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, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "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 }, - "array-initial": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", - "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", + "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": { - "array-slice": "^1.0.0", - "is-number": "^4.0.0" - }, "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, - "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/@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": { - "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 - } + "@xtuc/ieee754": "^1.2.0" } }, - "array-map": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", - "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", - "dev": true + "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, + "dependencies": { + "@xtuc/long": "4.2.2" + } }, - "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/@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 }, - "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/@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, + "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" + } }, - "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/@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": { - "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 - } + "@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" } }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "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": { - "array-uniq": "^1.0.1" + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true + "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, + "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" + } + }, + "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, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + "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, + "peerDependencies": { + "webpack": "4.x.x || 5.x.x", + "webpack-cli": "4.x.x" + } }, - "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/@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": { - "define-properties": "^1.1.2", - "es-abstract": "^1.10.0", - "function-bind": "^1.1.1" + "dependencies": { + "envinfo": "^7.7.3" + }, + "peerDependencies": { + "webpack-cli": "4.x.x" } }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "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, + "peerDependencies": { + "webpack-cli": "4.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "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 }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "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 }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + "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" + } }, - "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" + "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" } }, - "assert": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", - "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "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": { - "util": "0.10.3" + "engines": { + "node": ">=10.13.0" }, - "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" - } - } + "peerDependencies": { + "acorn": "^8.14.0" } }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "async-done": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.1.tgz", - "integrity": "sha512-R1BaUeJ4PMoLNJuk+0tLJgjmEqVsdN118+Z8O+alhnQDQgy0kmD5Mqi0DNEmMx2LM0Ed5yekKu+ZXYvIHceicg==", + "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": { - "end-of-stream": "^1.1.0", - "once": "^1.3.2", - "process-nextick-args": "^1.0.7", - "stream-exhaust": "^1.0.1" + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "async-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true - }, - "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==", - "dev": true - }, - "async-settle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", - "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", + "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": { - "async-done": "^1.2.2" + "engines": { + "node": ">=0.4.0" } }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", - "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=" - }, - "awesome-typescript-loader": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/awesome-typescript-loader/-/awesome-typescript-loader-5.2.1.tgz", - "integrity": "sha512-slv66OAJB8orL+UUaTI3pKlLorwIvS4ARZzYR9iJJyGsEgOqueMfOMdKySWzZ73vIkEe3fcwFgsKMg4d8zyb1g==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "enhanced-resolve": "^4.0.0", - "loader-utils": "^1.1.0", - "lodash": "^4.17.5", - "micromatch": "^3.1.9", - "mkdirp": "^0.5.1", - "source-map-support": "^0.5.3", - "webpack-log": "^1.2.0" + "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" + } + }, + "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": { - "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" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { "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" - } + "optional": true } } }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" - }, - "azure-storage": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/azure-storage/-/azure-storage-2.10.1.tgz", - "integrity": "sha512-rnFo1uMIPtilusRCpK91tfY3P4Q7qRsDNwriXdp+OeTIGkGt0cTxL4mhqYfNPYPK+WBQmBdGWhOk+iROM05dcw==", - "requires": { - "browserify-mime": "~1.2.9", - "extend": "~1.2.1", - "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": "0.4.3" - }, - "dependencies": { - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" - }, - "extend": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz", - "integrity": "sha1-oPX9bPyDpf5J72mNYOyKYk3UV2w=" - }, - "har-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", - "requires": { - "ajv": "^5.3.0", - "har-schema": "^2.0.0" - } - }, - "mime-db": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", - "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" - }, - "mime-types": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", - "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", - "requires": { - "mime-db": "~1.36.0" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "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" - }, - "dependencies": { - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - } - } - }, - "sax": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", - "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=" - }, - "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" - } - }, - "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" - } - }, - "xmlbuilder": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.3.tgz", - "integrity": "sha1-xGFLp04K0ZbmCcknLNnh3bKKilg=" - } + "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, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.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/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" + "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" } }, - "babel-loader": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.4.tgz", - "integrity": "sha512-fhBhNkUToJcW9nV46v8w87AJOwAJDz84c1CL57n3Stj73FANM/b9TbCUK4YhdOwEyZ+OxhYpdeZDNzSI29Firw==", + "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": { - "find-cache-dir": "^1.0.0", - "loader-utils": "^1.0.2", - "mkdirp": "^0.5.1", - "util.promisify": "^1.0.0" + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "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/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, - "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" + "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" } }, - "babel-plugin-inline-json-import": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/babel-plugin-inline-json-import/-/babel-plugin-inline-json-import-0.3.1.tgz", - "integrity": "sha512-cNQhU7de6V6IWJGwHuC5XJaGL3nu7RUCDAow/fXyHIAf/UNFkTkr/MR7+c1O8a01Z70XtTeq9mamfdU74LHW9A==", + "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 + }, + "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": { - "decache": "^4.4.0" + "peerDependencies": { + "ajv": "^6.9.1" } }, - "babel-plugin-macros": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.4.2.tgz", - "integrity": "sha512-NBVpEWN4OQ/bHnu1fyDaAaTPAjnhXCEPqr1RwqxrU7b6tZ2hypp+zX4hlNfmVGfClD5c3Sl6Hfj5TJNF5VG5aA==", + "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": { - "cosmiconfig": "^5.0.5", - "resolve": "^1.8.1" - }, "dependencies": { - "resolve": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", - "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", - "dev": true, - "requires": { - "path-parse": "^1.0.5" - } - } + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "babel-plugin-prismjs": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/babel-plugin-prismjs/-/babel-plugin-prismjs-1.0.2.tgz", - "integrity": "sha512-WbUE86Aih6h6daLpyavuikEXECrkon21oWh4MOHa5stMfY/IK1e/Sr79qEGhl7KrL16fMChB3tdbVR82ubnzOg==", - "dev": true - }, - "babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "http://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=", - "dev": true - }, - "babel-plugin-transform-runtime": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", - "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", + "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": { - "babel-runtime": "^6.22.0" + "engines": { + "node": ">=8" } }, - "babel-polyfill": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", - "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "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, - "requires": { - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "regenerator-runtime": "^0.10.5" - }, "dependencies": { - "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 - } + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" } }, - "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=", + "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, - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - } + "engines": { + "node": ">=0.10.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/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": { - "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 - } + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" } }, - "bach": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", - "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", + "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": { - "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" + "dependencies": { + "buffer-equal": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "bail": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.3.tgz", - "integrity": "sha512-1X8CnjFVQ+a+KW36uBNMTU5s8+v5FzeqrP7hTG5aTb4aPreSbZJlhwPon9VKMuEVgV++JM+SQrALY3kr7eswdg==", - "dev": true - }, - "balanced-match": { + "node_modules/append-buffer/node_modules/buffer-equal": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "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" + "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, + "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": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "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==", - "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==", - "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==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } + "@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 } } }, - "base16": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", - "integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA=", - "dev": true - }, - "base64-js": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", - "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=", - "dev": true + "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" + } + ] }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" + "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, + "license": "MIT", + "dependencies": { + "file-type": "^4.2.0" + }, + "engines": { + "node": ">=4" } }, - "beeper": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", - "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", - "dev": true - }, - "bfj": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.1.tgz", - "integrity": "sha512-+GUNvzHR4nRyGybQc2WpNJL4MJazMuvf92ueIyA0bIkPRwhhQu3IfZQ2PSoVPpCBJfmoSdOxu5rnotfFLlvYRQ==", + "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, - "requires": { - "bluebird": "^3.5.1", - "check-types": "^7.3.0", - "hoopy": "^0.1.2", - "tryer": "^1.0.0" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "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==", + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, - "binary-extensions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", - "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "node_modules/arg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", + "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", "dev": true }, - "bintrees": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", - "integrity": "sha1-SfiW1uhYpKSZ34XDj7OZua/4QPg=", - "dev": true - }, - "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "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": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - }, "dependencies": { - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "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" - } - } + "sprintf-js": "~1.0.2" } }, - "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/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": { - "inherits": "~2.0.0" + "engines": { + "node": ">=0.10.0" } }, - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", - "dev": true - }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true + "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, + "engines": { + "node": ">=0.10.0" + } }, - "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "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": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", - "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" - }, "dependencies": { - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "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 - }, - "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/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, + "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==", - "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=", - "requires": { - "is-extendable": "^0.1.0" - } - } + "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, + "dependencies": { + "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" } }, - "brorand": { + "node_modules/array-slice": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true - }, - "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", - "dev": true - }, - "browser-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": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", "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.10.0" } }, - "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/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": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" + "engines": { + "node": ">=8" } }, - "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/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": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" + "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" } }, - "browserify-mime": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/browserify-mime/-/browserify-mime-1.2.9.tgz", - "integrity": "sha1-rrGvKN5sDXpqLOQK22j/GEIq8x8=" - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "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": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" + "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" } }, - "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/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": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" + "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" } }, - "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/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": { - "pako": "~1.0.5" + "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" } }, - "browserslist": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.1.1.tgz", - "integrity": "sha512-VBorw+tgpOtZ1BYhrVSVTzTt/3+vSE3eFUh0N2GCFK1HffceOaf32YS/bs6WiFhjDAblAFrx85jMy3BG9fBK2Q==", + "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, - "requires": { - "caniuse-lite": "^1.0.30000884", - "electron-to-chromium": "^1.3.62", - "node-releases": "^1.0.0-alpha.11" + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" } }, - "buffer": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", - "integrity": "sha1-pyyTb3e5a/UvX357RnGAYoVR3vs=", + "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": { - "base64-js": "0.0.8", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" + "dependencies": { + "object-assign": "^4.1.1", + "util": "0.10.3" } }, - "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/assert/node_modules/inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "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": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" + "dependencies": { + "inherits": "2.0.1" } }, - "buffer-alloc-unsafe": { + "node_modules/assertion-error": { "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 + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } }, - "buffer-equal": { + "node_modules/assign-symbols": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", - "dev": true + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true, + "engines": { + "node": ">=0.10.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/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 }, - "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/async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", "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 + "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, + "dependencies": { + "end-of-stream": "^1.4.4", + "once": "^1.4.0", + "stream-exhaust": "^1.0.2" + }, + "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/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" + } }, - "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/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" + } }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "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" + } }, - "cacache": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", - "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==", + "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": { - "bluebird": "^3.5.1", - "chownr": "^1.0.1", - "glob": "^7.1.2", - "graceful-fs": "^4.1.11", - "lru-cache": "^4.1.1", - "mississippi": "^2.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.2", - "ssri": "^5.2.4", - "unique-filename": "^1.1.0", - "y18n": "^4.0.0" - }, "dependencies": { - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - } + "async-done": "^2.0.0" + }, + "engines": { + "node": ">= 10.13.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==", - "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/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "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/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": { - "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 - } + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", - "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/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": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" + "engines": { + "node": ">=4" } }, - "caniuse-lite": { - "version": "1.0.30000887", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000887.tgz", - "integrity": "sha512-AHpONWuGFWO8yY9igdXH94tikM6ERS84286r0cAMAXYFtJBk76lhiMhtCxBJNBZsD6hzlvpWZ2AtbVFEkf4JQA==", + "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 }, - "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/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": { - "get-proxy": "^2.0.0", - "isurl": "^1.0.0-alpha5", - "tunnel-agent": "^0.6.0", - "url-to-options": "^1.0.1" + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" } }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "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, - "optional": true, - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" } }, - "chai": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", - "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "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": { - "assertion-error": "^1.0.1", - "check-error": "^1.0.1", - "deep-eql": "^3.0.0", - "get-func-name": "^2.0.0", - "pathval": "^1.0.0", - "type-detect": "^4.0.0" - } + "hasInstallScript": true }, - "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==", + "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 }, - "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/bach": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", + "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", "dev": true, - "requires": { - "check-error": "^1.0.2" + "dependencies": { + "async-done": "^2.0.0", + "async-settle": "^2.0.0", + "now-and-later": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" } }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "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": { - "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": { + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "character-entities": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.2.tgz", - "integrity": "sha512-sMoHX6/nBiy3KKfC78dnEalnpn0Az0oSNvqUWYTtYrhRI5iUIYsROU48G+E+kMFQzqXaJ8kHJZ85n7y6/PHgwQ==", - "dev": true + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, - "character-entities-legacy": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.2.tgz", - "integrity": "sha512-9NB2VbXtXYWdXzqrvAHykE/f0QJxzaKIpZ5QzNZrrgQ7Iyxr2vnfS8fCBNVW9nUEZE0lo57nxKRqnzY/dKrwlA==", - "dev": true + "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 }, - "character-reference-invalid": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.2.tgz", - "integrity": "sha512-7I/xceXfKyUJmSAn/jw8ve/9DyOP7XxufNYLI9Px7CmsKgEUaZLUTax6nZxGQtaoiZCjpu6cHPj20xC/vqRReQ==", - "dev": 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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", - "dev": true + "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" + } }, - "charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + "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, + "dependencies": { + "bytesish": "^0.4.1", + "caseless": "~0.12.0", + "is-stream": "^2.0.0" + } }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true + "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, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "check-types": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/check-types/-/check-types-7.4.0.tgz", - "integrity": "sha512-YbulWHdfP99UfZ73NcUDlNJhEIDgm9Doq9GhpyXbF+7Aegi3CVV7qqMCKTTqJxlvEvnQBp9IA+dxsGN6xK/nSg==", - "dev": true + "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, + "engines": { + "node": "*" + } }, - "cheerio": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz", - "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=", + "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": { - "css-select": "~1.2.0", - "dom-serializer": "~0.1.0", - "entities": "~1.1.1", - "htmlparser2": "^3.9.1", - "lodash": "^4.15.0", - "parse5": "^3.0.1" + "engines": { + "node": ">=8" } }, - "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "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": { - "anymatch": "^1.3.0", - "async-each": "^1.0.0", - "fsevents": "^1.0.0", - "glob-parent": "^2.0.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^2.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0" - }, + "license": "MIT", "dependencies": { - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true, - "requires": { - "is-glob": "^2.0.0" - } - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - } + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" } }, - "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", - "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" }, - "chrome-trace-event": { + "node_modules/boolbase": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz", - "integrity": "sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "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" + } + }, + "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": { - "tslib": "^1.9.0" + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "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/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/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": { + "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" } }, - "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 - }, - "clap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", - "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", + "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": { - "chalk": "^1.1.3" + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" } }, - "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==", - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, + "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, "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=", - "requires": { - "is-descriptor": "^0.1.0" - } - } + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" } }, - "classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", - "dev": true - }, - "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/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": { - "source-map": "~0.6.0" - }, + "license": "MIT", "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 - } + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, - "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/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": { - "restore-cursor": "^2.0.0" - } + "license": "MIT" }, - "cli-table": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", - "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "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": { - "colors": "1.0.3" - }, - "dependencies": { - "colors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", - "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" } - } - }, - "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 + ], + "license": "MIT" }, - "clipboard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.1.tgz", - "integrity": "sha512-7yhQBmtN+uYZmfRjjVjKa0dZdWuabzpSKGtyQZN+9C8xlC788SSJjOHWh7tzurfwTqTD5UDYAhIv5fRJg3sHjQ==", + "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, - "optional": true, - "requires": { - "good-listener": "^1.2.2", - "select": "^1.1.2", - "tiny-emitter": "^2.0.0" + "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" } }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "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": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true + "license": "MIT" + }, + "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, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "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" - } + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" } + ] + }, + "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, + "dependencies": { + "pako": "~1.0.5" } }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true - }, - "clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", - "dev": true - }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "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": { - "mimic-response": "^1.0.0" + "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": { + "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" } }, - "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 - }, - "cloneable-readable": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.2.tgz", - "integrity": "sha512-Bq6+4t+lbM8vhTs/Bef5c5AdEMtapp/iFb6+s4/Hh9MVTt8OLKH7ZOOZSCT+Ys7hsHvqv0GuMPJ1lnQJVHvxpg==", + "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": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - }, - "dependencies": { - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "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" - } + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "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": "consulting", + "url": "https://feross.org/support" } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" } }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "coa": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", - "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", + "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, - "requires": { - "q": "^1.1.2" + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" } }, - "code-point-at": { + "node_modules/buffer-alloc-unsafe": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true, + "license": "MIT" }, - "codecov": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.0.2.tgz", - "integrity": "sha512-9ljtIROIjPIUmMRqO+XuDITDoV8xRrZmA0jcEq6p2hg2+wY9wGmLfreAZGIL72IzUfdEDZaU8+Vjidg1fBQ8GQ==", + "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, - "requires": { - "argv": "0.0.2", - "request": "^2.81.0", - "urlgrey": "0.4.4" + "engines": { + "node": "*" } }, - "collapse-white-space": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.4.tgz", - "integrity": "sha512-YfQ1tAUZm561vpYD+5eyWN8+UsceQbSrqqlc/6zDY2gtAE+uZLSdkkovhnGpmCThsvKBFakq4EdY/FF93E8XIw==", + "node_modules/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 }, - "collection-map": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", - "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", - "dev": true, - "requires": { - "arr-map": "^2.0.2", - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - } - }, - "collection-visit": { + "node_modules/buffer-fill": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", "dev": true, - "requires": { - "color-name": "^1.1.1" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "license": "MIT" }, - "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==", + "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 }, - "colors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.0.tgz", - "integrity": "sha512-EDpX3a7wHMWFA7PUHWPHNWqOxIIRSJetuwl0AS5Oi/5FMV8kWm69RTlgm00GKjBO1xFHMtBbL49yRtMMdticBw==", + "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 }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "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 }, - "commandpost": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/commandpost/-/commandpost-1.3.0.tgz", - "integrity": "sha512-T62tyrmYTkaRDbV2z1k2yXTyxk0cFptXYwo1cUbnfHtp7ThLgQ9/90jG1Ym5WLZgFhvOTaHA5VSARWJ9URpLDw==", + "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 }, - "comment-json": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-1.1.3.tgz", - "integrity": "sha1-aYbDMw/uDEyeAMI5jNYa+l2PI54=", - "requires": { - "json-parser": "^1.0.0" + "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, + "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" } }, - "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.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "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": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - }, - "dependencies": { - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://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" - } - } + "license": "MIT", + "engines": { + "node": ">=4" } }, - "config-chain": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz", - "integrity": "sha1-q6CXR9++TD5w52am5BWG4YWfxvI=", + "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, - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "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": { - "date-now": "^0.1.4" + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "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/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, + "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" + } }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", - "dev": true + "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" + } }, - "content-type": { + "node_modules/call-bound": { "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 - }, - "convert-source-map": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", - "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", - "dev": true - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", - "dev": true - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "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" + "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" } }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } }, - "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/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, - "requires": { - "each-props": "^1.3.0", - "is-plain-object": "^2.0.1" + "engines": { + "node": ">=6" } }, - "copy-webpack-plugin": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-4.6.0.tgz", - "integrity": "sha512-Y+SQCF+0NoWQryez2zXn5J5knmr9z/9qSQt7fbL78u83rxmigOy8X5+BFn8CFSuX+nKT8gpYwJX68ekqtQt6ZA==", + "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, - "requires": { - "cacache": "^10.0.4", - "find-cache-dir": "^1.0.0", - "globby": "^7.1.1", - "is-glob": "^4.0.0", - "loader-utils": "^1.1.0", - "minimatch": "^3.0.4", - "p-limit": "^1.0.0", - "serialize-javascript": "^1.4.0" - }, - "dependencies": { - "globby": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", - "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "dir-glob": "^2.0.0", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - } + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" }, - "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" - } + { + "type": "github", + "url": "https://github.com/sponsors/ai" } - } + ] }, - "core-js": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==", + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "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=" - }, - "cosmiconfig": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.6.tgz", - "integrity": "sha512-6DWfizHriCrFWURP1/qyhsiFvYdlJzbCzmtFWh744+KyWsJo5+kPzUZZaMRSSItoYc0pxFX7gEO7ZC1/gN/7AQ==", + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", "dev": true, - "requires": { - "is-directory": "^0.3.1", - "js-yaml": "^3.9.0", - "parse-json": "^4.0.0" - }, "dependencies": { - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - } + "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" } }, - "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/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": { - "bn.js": "^4.1.0", - "elliptic": "^6.0.0" + "engines": { + "node": ">=0.10" } }, - "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/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": { - "@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": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 5" } }, - "create-hash": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "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, - "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": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" } }, - "create-hmac": { - "version": "1.1.7", - "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", "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" + "engines": { + "node": "*" } }, - "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/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", "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" + "engines": { + "node": "*" } }, - "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/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": { - "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": { + "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" } }, - "css": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/css/-/css-2.2.3.tgz", - "integrity": "sha512-0W171WccAjQGGTKLhw4m2nnl0zPHUlTO/I8td4XzJgIB8Hg3ZZx71qT4G4eX8OVsSiaAKiUMy73E3nsbPlg2DQ==", + "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": { - "inherits": "^2.0.1", - "source-map": "^0.1.38", - "source-map-resolve": "^0.5.1", - "urix": "^0.1.0" - }, "dependencies": { - "source-map": { - "version": "0.1.43", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", - "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "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": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - } + "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" } }, - "css-select": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", + "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, - "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" - } - } + "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" } }, - "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/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": { - "cssesc": "^0.1.0", - "fastparse": "^1.1.1", - "regexpu-core": "^1.0.0" + "engines": { + "node": ">= 6" }, - "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" - } - } + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "css-tree": { - "version": "1.0.0-alpha25", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha25.tgz", - "integrity": "sha512-XC6xLW/JqIGirnZuUWHXCHRaAjje2b3OIB0Vj5RIJo6mIi/AdJo30quQl5LxUl0gkXDIrTrFGbMlcZjyFplz1A==", + "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, - "requires": { - "mdn-data": "^1.0.0", - "source-map": "^0.5.3" + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "css-what": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.0.tgz", - "integrity": "sha1-lGfQMsOM+u+58teVASUwYvh/ob0=", - "dev": true - }, - "cssesc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", - "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", - "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" + } + ] }, - "csso": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-3.4.0.tgz", - "integrity": "sha1-V7J+9VPMy/WqlkxkF0hkHprxE/M=", + "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": { - "css-tree": "1.0.0-alpha25" + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "cssom": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.4.tgz", - "integrity": "sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog==", - "dev": true - }, - "cssstyle": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.1.1.tgz", - "integrity": "sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog==", + "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, - "requires": { - "cssom": "0.3.x" + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "csstype": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.5.7.tgz", - "integrity": "sha512-Nt5VDyOTIIV4/nRFswoCKps1R5CD1hkiyjBE9/thNaNZILLEviVw9yWQw15+O+CpNjQKB/uvdcxFFOrSflY3Yw==", - "dev": true - }, - "cyclist": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", - "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", - "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" + } }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "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": { - "es5-ext": "^0.10.9" + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "d3-array": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", - "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", - "dev": true + "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" + } + ] }, - "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/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": { - "d3-quadtree": "1.0.1" + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "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/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": { - "d3-dispatch": "1", - "d3-drag": "1", - "d3-interpolate": "1", - "d3-selection": "1", - "d3-transition": "1" + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "d3-chord": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", - "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", + "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, - "requires": { - "d3-array": "1", - "d3-path": "1" + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "d3-collection": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", - "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", + "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, + "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" + } + }, + "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 }, - "d3-color": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.2.3.tgz", - "integrity": "sha512-x37qq3ChOTLd26hnps36lexMRhNXEtVxZ4B25rL0DVdDsGQIJGB18S7y9XDwlDD6MD/ZBzITCf4JjGMM10TZkw==", + "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 }, - "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/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, - "requires": { - "d3-array": "^1.1.1" + "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" } }, - "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/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, - "requires": { - "d3-dispatch": "1", - "d3-selection": "1" + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, - "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 + "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 }, - "d3-force": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.1.2.tgz", - "integrity": "sha512-p1vcHAUF1qH7yR+e8ip7Bs61AHjLeKkIn8Z2gzwU2lwEf2wkSpWdjXG0axudTHsVFnYGlMkFaEsVy2l8tAg1Gw==", + "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": { - "d3-collection": "1", - "d3-dispatch": "1", - "d3-quadtree": "1", - "d3-timer": "1" + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "node": ">=6.0" } }, - "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-glyphedge": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/d3-glyphedge/-/d3-glyphedge-1.2.0.tgz", - "integrity": "sha512-F49fyMXMLYDHvqvxSmuGZrtIWeWLZWxar82WL1CJDBDPk4z6GUGSG4wX7rdv7N7R/YazAyMMnpOL0YQcmTLlOQ==", - "dev": true - }, - "d3-hexbin": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/d3-hexbin/-/d3-hexbin-0.2.2.tgz", - "integrity": "sha1-nFg32s/UcasFM3qeke8Qv8T5iDE=", - "dev": true - }, - "d3-hierarchy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz", - "integrity": "sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w==", - "dev": true - }, - "d3-interpolate": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz", - "integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==", + "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": { - "d3-color": "1" + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" } }, - "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 + "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, + "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" }, - "d3-polygon": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz", - "integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w==", + "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 }, - "d3-quadtree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.1.tgz", - "integrity": "sha1-E74CViTxEEBe1DU2xQaq7Bme1ZE=", - "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==" }, - "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==", + "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": { - "d3-array": "^1.2.1", - "d3-collection": "^1.0.4", - "d3-shape": "^1.2.0" + "engines": { + "node": ">=6" } }, - "d3-scale": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-1.0.7.tgz", - "integrity": "sha512-KvU92czp2/qse5tUfGms6Kjig0AhHOwkzXG0+PqIJB3ke0WUv088AHMZI0OssO9NCkXt4RP8yju9rpH8aGB7Lw==", + "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": { - "d3-array": "^1.2.0", - "d3-collection": "1", - "d3-color": "1", - "d3-format": "1", - "d3-interpolate": "1", - "d3-time": "1", - "d3-time-format": "2" + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "d3-selection": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.3.2.tgz", - "integrity": "sha512-OoXdv1nZ7h2aKMVg3kaUFbLLK5jXUFAMLD/Tu5JA96mjf8f2a9ZUESGY+C36t8R1WFeWk/e55hy54Ml2I62CRQ==", - "dev": true + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true, + "engines": { + "node": ">=0.8" + } }, - "d3-shape": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.2.2.tgz", - "integrity": "sha512-hUGEozlKecFZ2bOSNt7ENex+4Tk9uc/m0TtTEHBvitCBxUNjhzm5hS2GrrVRD/ae4IylSmxGeqX5tWC2rASMlQ==", + "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": { - "d3-path": "1" + "engines": { + "node": ">= 0.10" } }, - "d3-time": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.10.tgz", - "integrity": "sha512-hF+NTLCaJHF/JqHN5hE8HVGAXPStEq6/omumPE/SxyHVrR7/qQxusFDo0t0c/44+sCGHthC7yNGFZIEgju0P8g==", - "dev": true + "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, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } }, - "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/clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", "dev": true, - "requires": { - "d3-time": "1" + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" } }, - "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==", + "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 }, - "d3-transition": { + "node_modules/cloneable-readable": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.1.3.tgz", - "integrity": "sha512-tEvo3qOXL6pZ1EzcXxFcPNxC/Ygivu5NoBY6mbzidATAeML86da+JfVIUzon3dNM6UX6zjDx+xbYDmMVtTSjuA==", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", "dev": true, - "requires": { - "d3-color": "1", - "d3-dispatch": "1", - "d3-ease": "1", - "d3-interpolate": "1", - "d3-selection": "^1.1.0", - "d3-timer": "1" + "dependencies": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" } }, - "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/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" + } }, - "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/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" } }, - "data-urls": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.0.1.tgz", - "integrity": "sha512-0HdcMZzK6ubMUnsMmQmG0AcLQPvbvb47R0+7CCZQCYgcd8OUWG91CG7sM6GoXgjz+WLl4ArFzHtBMy/QqSF4eg==", + "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": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.1.0", - "whatwg-url": "^7.0.0" + "engines": { + "node": ">=16" } }, - "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/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, + "dependencies": { + "color-name": "1.1.3" + } }, - "dateformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", - "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", + "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 }, - "debounce-hashed": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/debounce-hashed/-/debounce-hashed-0.1.2.tgz", - "integrity": "sha1-oN/jB8Gn2zD2kRyM+8DvhB831K8=", + "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 }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" + "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": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "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/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 + }, + "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 + }, + "node_modules/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/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": { - "debug": "3.X", - "memoizee": "0.4.X", - "object-assign": "4.X" - }, + "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" - } - } + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" } }, - "decache": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/decache/-/decache-4.4.0.tgz", - "integrity": "sha1-b232uF1+fEQQqTL/wmSJt46azRM=", + "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": { - "callsite": "^1.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" + } + ], + "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" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "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 }, - "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=" - }, - "decompress": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", - "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", + "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, - "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": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } + "each-props": "^3.0.0", + "is-plain-object": "^5.0.0" + }, + "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=", + "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, - "requires": { - "mimic-response": "^1.0.0" + "engines": { + "node": ">=0.10.0" } }, - "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/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": { - "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 - } + "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" } }, - "decompress-tarbz2": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", - "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "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": { - "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 - } + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.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/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, - "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 - } + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, - "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/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 + }, + "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": { - "file-type": "^3.8.0", - "get-stream": "^2.2.0", - "pify": "^2.3.0", - "yauzl": "^2.4.2" - }, + "license": "MIT", "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 - } + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" } }, - "deep-assign": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/deep-assign/-/deep-assign-1.0.0.tgz", - "integrity": "sha1-sJJ0O+hCfcYh6gBnzex+cN0Z83s=", + "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, - "requires": { - "is-obj": "^1.0.0" + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" } }, - "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/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": { - "type-detect": "^4.0.0" + "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" } }, - "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 - }, - "deepmerge": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.1.1.tgz", - "integrity": "sha512-urQxA1smbLZ2cBbXbaYObM1dJ82aJ2H57A1C/Kklfh/ZN1bgH4G/n5KWhdNfOK11W98gqZfyYj7W4frJJRwA2w==", + "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 }, - "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/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": { - "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 - } + "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" } }, - "default-resolution": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", - "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", - "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" + } }, - "define-properties": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "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": { - "foreach": "^2.0.5", - "object-keys": "^1.0.8" + "engines": { + "node": ">=8" } }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, + "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": { - "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==", - "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==", - "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==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "del": { + "node_modules/cross-env/node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", - "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "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" + "engines": { + "node": ">=8" } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegate": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", - "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "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, - "optional": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, - "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "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" } }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true - }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", - "dev": true - }, - "detect-indent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", - "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=", - "dev": true - }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", - "dev": true - }, - "detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "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": { - "address": "^1.0.1", - "debug": "^2.6.0" + "bin": { + "semver": "bin/semver" } }, - "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/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, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, - "diagnostic-channel-publishers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz", - "integrity": "sha1-ji1geottef6IC1SLxYzGvrKIxPM=" - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "diff-match-patch": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.0.tgz", - "integrity": "sha1-HMPIOkkNZ/ldkeOfatHy4Ia2MEg=" - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "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": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" + "engines": { + "node": "*" } }, - "dir-glob": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", - "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", + "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": { - "arrify": "^1.0.1", - "path-type": "^3.0.0" - }, + "license": "MIT", "dependencies": { - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - } + "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" } }, - "discontinuous-range": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", - "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=", + "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 }, - "doctrine": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz", - "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=", + "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": { - "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 - } + "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" } }, - "dom-converter": { - "version": "0.1.4", - "resolved": "http://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz", - "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=", + "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": { - "utila": "~0.3" - }, "dependencies": { - "utila": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", - "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", - "dev": true - } + "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" } }, - "dom-serializer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", - "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "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, - "requires": { - "domelementtype": "~1.1.1", - "entities": "~1.1.1" + "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" + } + }, + "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": { - "domelementtype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", - "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", - "dev": true - } + "ms": "2.0.0" } }, - "dom-walk": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", - "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=", + "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 }, - "domain-browser": { + "node_modules/decamelize": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true - }, - "domelementtype": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", - "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", - "dev": true - }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true, - "requires": { - "webidl-conversions": "^4.0.2" + "engines": { + "node": ">=0.10.0" } }, - "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "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": { - "domelementtype": "1" + "license": "MIT", + "engines": { + "node": ">=0.10" } }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "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": { - "dom-serializer": "0", - "domelementtype": "1" + "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" } }, - "dotenv": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz", - "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==" - }, - "download": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/download/-/download-7.0.0.tgz", - "integrity": "sha512-0Fe/CAjKycx12IG9We9gYlLP03BEcWTpttg7P5mwfOiQTg584kpuHqP7F61RkUJM+mfEdEU9TJonm0PJp5rQLw==", + "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, - "requires": { - "caw": "^2.0.1", - "content-disposition": "^0.5.2", - "decompress": "^4.2.0", - "ext-name": "^5.0.0", - "file-type": "^7.7.1", - "filenamify": "^2.0.0", - "get-stream": "^3.0.0", - "got": "^8.3.1", - "make-dir": "^1.2.0", - "p-event": "^1.3.0", - "pify": "^3.0.0" + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" } }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", - "dev": true - }, - "duplexer2": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", - "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "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, - "requires": { - "readable-stream": "~1.1.9" - }, + "license": "MIT", "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - } + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" } }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true - }, - "duplexify": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", - "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", + "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, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - }, - "dependencies": { - "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==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - } + "license": "MIT", + "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/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": { - "is-plain-object": "^2.0.1", - "object.defaults": "^1.1.0" + "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" } }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "~0.1.0" + "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" } }, - "editorconfig": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.13.3.tgz", - "integrity": "sha512-WkjsUNVCu+ITKDj73QDvi0trvpdDWdkDyHybDGSXPfekLCqwmpD7CP7iPbvBgosNuLcI96XTDwNa75JyFl7tEQ==", + "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, - "requires": { - "bluebird": "^3.0.5", - "commander": "^2.9.0", - "lru-cache": "^3.2.0", - "semver": "^5.1.0", - "sigmund": "^1.0.1" - }, + "license": "MIT", "dependencies": { - "lru-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-3.2.0.tgz", - "integrity": "sha1-cXibO39Tmb7IVl3aOKow0qCX7+4=", - "dev": true, - "requires": { - "pseudomap": "^1.0.1" - } - } + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" } }, - "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.6.1", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", - "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.3.71", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.71.tgz", - "integrity": "sha512-VjZ6mQbbgF3GZ3eeQOMMgkdP8pWAHoW9UA+CNAVB4qSaOES4usB9RVIW764mYffdT2GOWF10Udt82RIZnTCTMg==", - "dev": true - }, - "elliptic": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", - "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", + "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, - "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" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "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/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": { - "babel-plugin-emotion": "^9.2.11", - "create-emotion": "^9.2.12" + "license": "MIT", + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" } }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true - }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "~0.4.13" + "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" } }, - "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==", + "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, - "requires": { - "once": "^1.4.0" + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.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==", + "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, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", - "tapable": "^1.0.0" + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "entities": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", - "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", - "dev": true - }, - "enzyme": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.7.0.tgz", - "integrity": "sha512-QLWx+krGK6iDNyR1KlH5YPZqxZCQaVF6ike1eDJAOg0HvSkSCVImPsdWaNw6v+VrnK92Kg8jIOYhuOSS9sBpyg==", + "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": { - "array.prototype.flat": "^1.2.1", - "cheerio": "^1.0.0-rc.2", - "function.prototype.name": "^1.1.0", - "has": "^1.0.3", - "is-boolean-object": "^1.0.0", - "is-callable": "^1.1.4", - "is-number-object": "^1.0.3", - "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" - }, + "license": "MIT", "dependencies": { - "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 - } + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "enzyme-adapter-react-16": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.6.0.tgz", - "integrity": "sha512-ay9eGFpChyUDnjTFMMJHzrb681LF3hPWJLEA7RoLFG9jSWAdAm2V50pGmFV9dYGJgh5HfdiqM+MNvle41Yf/PA==", + "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": { - "enzyme-adapter-utils": "^1.8.0", - "function.prototype.name": "^1.1.0", - "object.assign": "^4.1.0", - "object.values": "^1.0.4", - "prop-types": "^15.6.2", - "react-is": "^16.5.2", - "react-test-renderer": "^16.0.0-0" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "enzyme-adapter-utils": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.8.1.tgz", - "integrity": "sha512-s3QB3xQAowaDS2sHhmEqrT13GJC4+n5bG015ZkLv60n9k5vhxxHTQRIneZmQ4hmdCZEBrvUJ89PG6fRI5OEeuQ==", + "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, - "requires": { - "function.prototype.name": "^1.1.0", - "object.assign": "^4.1.0", - "prop-types": "^15.6.2" + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "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, - "requires": { - "prr": "~1.0.1" + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" } }, - "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "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, - "requires": { - "is-arrayish": "^0.2.1" + "optional": true, + "engines": { + "node": ">=4.0.0" } }, - "es-abstract": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", - "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "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 + }, + "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": { - "es-to-primitive": "^1.1.1", - "function-bind": "^1.1.1", - "has": "^1.0.1", - "is-callable": "^1.1.3", - "is-regex": "^1.0.4" + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "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/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": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "engines": { + "node": ">=8" } }, - "es5-ext": { - "version": "0.10.43", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.43.tgz", - "integrity": "sha512-cZd1vezWuTM5qMlasKWqQFioFKwO352nVBzhOTMUf/pKQl5Gcq5EdJzqtSNXKnFQSCJDiQZjCYlYbnzFB657OA==", + "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, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "1" + "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" } }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "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, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" + "engines": { + "node": ">=8" } }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "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": { - "d": "1", - "es5-ext": "~0.10.14" + "dependencies": { + "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" } }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "node_modules/del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.14", - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" + "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" } }, - "escape-carriage": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.2.0.tgz", - "integrity": "sha1-Nc5Rp5YO/LWui7Wbg/VmqYyUbf0=", - "dev": true - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "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" + } }, - "escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "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, - "requires": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.2.0" - }, "dependencies": { - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "dev": true, - "optional": true, - "requires": { - "amdefine": ">=0.0.4" - } - } + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" } }, - "eslint-scope": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz", - "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==", + "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, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - }, - "dependencies": { - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - } + "engines": { + "node": ">=0.10.0" } }, - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" - }, - "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/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": { - "estraverse": "^4.1.0" - }, - "dependencies": { - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - } + "optional": true, + "engines": { + "node": ">=8" } }, - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true + "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": { + "semver": "^7.5.3" + } }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "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": "*" + } }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "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, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" } }, - "event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "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, - "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": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" } }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", - "dev": true - }, - "eventsource": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-0.1.6.tgz", - "integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=", + "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, - "requires": { - "original": ">=0.0.5" + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "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/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" } }, - "execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "node_modules/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, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^3.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" + "engines": { + "node": ">=0.4", + "npm": ">=1.2" } }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "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=", - "requires": { - "is-extendable": "^0.1.0" - } - } + "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, + "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" } }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "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": { - "fill-range": "^2.1.0" - }, + "license": "MIT", "dependencies": { - "fill-range": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", - "dev": true, - "requires": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^3.0.0", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" - } - }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "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" - } - }, - "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" - } - } + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" } }, - "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/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, - "requires": { - "homedir-polyfill": "^1.0.1" + "license": "ISC", + "bin": { + "semver": "bin/semver" } }, - "express": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", - "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", - "dev": true, - "requires": { - "accepts": "~1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.3", - "content-disposition": "0.5.2", - "content-type": "~1.0.4", - "cookie": "0.3.1", - "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.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.4", - "qs": "6.5.2", - "range-parser": "~1.2.0", - "safe-buffer": "5.1.2", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "node_modules/dunder-proto": { + "version": "1.0.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" } }, - "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/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": { - "mime-db": "^1.28.0" - } + "license": "BSD-3-Clause" }, - "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/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "dev": true, - "requires": { - "ext-list": "^2.0.0", - "sort-keys-length": "^1.0.0" + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" } }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, + "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, "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==", - "requires": { - "is-plain-object": "^2.0.4" - } - } + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "external-editor": { - "version": "2.2.0", - "resolved": "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "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": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" - }, - "dependencies": { - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - } + "engines": { + "node": ">=0.10.0" } }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "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" - }, + "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, + "license": "MIT" + }, + "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, "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "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=", - "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==", - "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==", - "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==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } + "safe-buffer": "^5.0.1" } }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "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 }, - "fancy-log": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.2.tgz", - "integrity": "sha1-9BEl49hPLn2JpD0G2VjI94vha+E=", + "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, - "requires": { - "ansi-gray": "^0.1.1", - "color-support": "^1.1.3", - "time-stamp": "^1.0.0" + "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" } }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, - "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=", - "dev": true - }, - "fastparse": { + "node_modules/emitter-listener": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", - "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "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" + } + }, + "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 }, - "faye-websocket": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", - "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", + "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, - "requires": { - "websocket-driver": ">=0.5.1" + "engines": { + "node": ">= 4" } }, - "fbjs": { - "version": "0.8.17", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", - "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", + "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, - "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": { - "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 - }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dev": true, - "requires": { - "asap": "~2.0.3" - } - } + "once": "^1.4.0" } }, - "fd-slicer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", - "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "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, - "requires": { - "pend": "~1.2.0" + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" } }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "file-loader": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-2.0.0.tgz", - "integrity": "sha512-YCsBfd1ZGCyonOKLxPiKPdu+8ld9HAaMEvJewzz+b2eTF7uL5Zm/HdBF6FjCrpCMRq25Mi0U1gl4pwn2TlH7hQ==", + "node_modules/envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", "dev": true, - "requires": { - "loader-utils": "^1.0.2", - "schema-utils": "^1.0.0" + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" } }, - "file-matcher": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/file-matcher/-/file-matcher-1.3.0.tgz", - "integrity": "sha512-3CYUK4tsa+ssJZc0mzGF8AAh+8uMAFhiHMw9thRejqEMhTTuusC9UPTDA/NVn4msdXTC1b66HQq55VCh7CuZog==", - "requires": { - "micromatch": "^3.1.10" + "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" } }, - "file-type": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-7.7.1.tgz", - "integrity": "sha512-bTrKkzzZI6wH+NXhyD3SOXtb2zXTw2SbwI2RxUlRcXVsnN7jNL5hJzVQLYv7FOQhxFkK4XWdAflEaWFpaLLWpQ==", - "dev": true + "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" + } }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "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" + } }, - "filename-reserved-regex": { + "node_modules/es-module-lexer": { "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=", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true }, - "filenamify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.0.0.tgz", - "integrity": "sha1-vRYiYsC26Uv7zc8Zo7uzdk94VpU=", - "dev": true, - "requires": { - "filename-reserved-regex": "^2.0.0", - "strip-outer": "^1.0.0", - "trim-repeated": "^1.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": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" } }, - "filesize": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.5.11.tgz", - "integrity": "sha512-ZH7loueKBoDb7yG9esn1U+fgq7BzlzW6NRi5/rMdxIZ05dj7GFD/Xc5rq2CDt5Yq86CyfSYVyx4242QQNZbx1g==", - "dev": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.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": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "finalhandler": { - "version": "1.1.1", - "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "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": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", - "unpipe": "~1.0.0" + "dependencies": { + "hasown": "^2.0.0" } }, - "find-cache-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", - "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "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": { - "commondir": "^1.0.1", - "make-dir": "^1.0.0", - "pkg-dir": "^2.0.0" + "dependencies": { + "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" } }, - "find-root": { + "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/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", + "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=", "dev": true }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "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": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" + "engines": { + "node": ">=6" } }, - "findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "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": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" + "engines": { + "node": ">=0.8.0" } }, - "fined": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz", - "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=", + "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": { - "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" + "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" } }, - "first-chunk-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", - "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=", - "dev": true - }, - "flagged-respawn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.0.tgz", - "integrity": "sha1-Tnmumy6zi/hrO7Vr8+ClaqX8q9c=", - "dev": true - }, - "flat": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.0.0.tgz", - "integrity": "sha512-ji/WMv2jdsE+LaznpkIF9Haax0sdpTBozrz/Dtg4qSRMfbs8oVg4ypJunIRYPiMLvH/ed6OflXbnbTIKJhtgeg==", + "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": { - "is-buffer": "~1.1.5" + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" } }, - "flush-write-stream": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", - "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", + "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": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.4" + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" } }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "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": { - "for-in": "^1.0.1" + "dependencies": { + "ms": "^2.1.1" } }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true + "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, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "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, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } }, - "form-data": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "1.0.6", - "mime-types": "^2.1.12" + "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" } }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "dev": true + "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, + "dependencies": { + "ms": "^2.1.1" + } }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "^0.2.2" + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true + "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, + "bin": { + "semver": "bin/semver.js" + } }, - "from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", - "dev": true + "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, + "dependencies": { + "@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" + } }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "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": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" + "dependencies": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + }, + "engines": { + "node": ">=6.0" } }, - "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/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 }, - "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/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "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/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": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" + "license": "MIT", + "engines": { + "node": ">=5.0.0" } }, - "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/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": { - "async": "*" + "dependencies": { + "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" } }, - "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/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": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "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, + "engines": { + "node": ">=4.0" + } }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "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, - "optional": true, - "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.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": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "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 - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "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.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "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.2", - "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.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "^2.1.0" - } - }, - "ignore-walk": { - "version": "3.0.1", - "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.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": 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, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.1.1", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + } + }, + "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" + } + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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" + } + }, + "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" + }, + "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, + "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/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" + } + }, + "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 + }, + "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, + "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": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.1.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.5.1", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "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, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true } } }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "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, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "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/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": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "is-callable": "^1.1.3" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "fuzzy": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", - "integrity": "sha1-THbsL/CsGjap3M+aAN+GIweNTtg=" - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, - "get-port": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", - "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" + "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, + "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" + } }, - "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/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, - "requires": { - "npm-conf": "^1.1.0" + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" } }, - "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/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" + } }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + "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" + } }, - "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/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" } }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "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/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" } }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "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, - "requires": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - }, "dependencies": { - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true, - "requires": { - "is-glob": "^2.0.0" - } - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - } + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "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, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "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, - "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" - }, + "license": "ISC", "dependencies": { - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://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" - } - } + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "glob-watcher": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.1.tgz", - "integrity": "sha512-fK92r2COMC199WCyGUblrZKhjra3cyVMDiypDdqg1vsSDmexnbYivK1kNR4QItiNXLKmGlqan469ks67RtNa2g==", + "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, - "requires": { - "async-done": "^1.2.0", - "chokidar": "^2.0.0", - "just-debounce": "^1.0.0", - "object.defaults": "^1.1.0" - }, + "license": "MIT", "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "chokidar": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", - "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.2.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "lodash.debounce": "^4.0.8", - "normalize-path": "^2.1.1", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.5" - } - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - } + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "global": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", - "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", + "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, - "requires": { - "min-document": "^2.19.0", - "process": "~0.5.1" - }, + "license": "MIT", "dependencies": { - "process": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", - "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=", - "dev": true - } + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "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, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" + "engines": { + "node": ">=8" } }, - "global-modules-path": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/global-modules-path/-/global-modules-path-2.3.0.tgz", - "integrity": "sha512-HchvMJNYh9dGSCy8pOQ2O8u/hoXaL+0XhnrwH0RyLiSXMMTl9W3N6KUU73+JFOg5PGjtzl6VZzUQsnrpm7Szag==", - "dev": true + "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, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } }, - "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/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, - "requires": { - "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": ">=8" } }, - "globals": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz", - "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==", - "dev": true + "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": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "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": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "BSD-2-Clause", "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } + "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" } }, - "glogg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.1.tgz", - "integrity": "sha512-ynYqXLoluBKf9XGR1gA59yEJisIL7YHEH4xr3ZziHB5/yl4qWfaK8Js9jGe6gBGCSCKVqiyO30WnRZADvemUNw==", + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, - "requires": { - "sparkles": "^1.0.0" + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" } }, - "good-listener": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", - "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", + "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, - "optional": true, - "requires": { - "delegate": "^3.1.2" + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" } }, - "got": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/got/-/got-8.3.1.tgz", - "integrity": "sha512-tiLX+bnYm5A56T5N/n9Xo89vMaO1mrS9qoDqj3u/anVooqGozvY/HbXzEpDfbNeKsHCBpK40gSbz8wGYSp3i1w==", + "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, - "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": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" } }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + "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, + "engines": { + "node": ">=4.0" + } }, - "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 + "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" + } }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true + "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" + } }, - "gulp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.0.tgz", - "integrity": "sha1-lXZsYB2t5Kd+0+eyttwDiBtZY2Y=", + "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, - "requires": { - "glob-watcher": "^5.0.0", - "gulp-cli": "^2.0.0", - "undertaker": "^1.0.0", - "vinyl-fs": "^3.0.0" + "engines": { + "node": ">=0.8.x" + } + }, + "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" + } + }, + "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" + } + }, + "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": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "gulp-cli": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.0.1.tgz", - "integrity": "sha512-RxujJJdN8/O6IW2nPugl7YazhmrIEjmiVfPKrWt68r71UCaLKS71Hp0gpKT+F6qOUFtr7KqtifDKaAJPRVvMYQ==", - "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": "^2.5.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" - } - }, - "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 - }, - "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" - } - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "dev": true, - "requires": { - "invert-kv": "^1.0.0" - } - }, - "os-locale": { - "version": "1.4.0", - "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "dev": true, - "requires": { - "lcid": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "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 - }, - "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" - } - }, - "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" - } - } + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, - "gulp-chmod": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/gulp-chmod/-/gulp-chmod-2.0.0.tgz", - "integrity": "sha1-AMOQuSigeZslGsz2MaoJ4BzGKZw=", + "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": { - "deep-assign": "^1.0.0", - "stat-mode": "^0.2.0", - "through2": "^2.0.0" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "gulp-debounced-watch": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/gulp-debounced-watch/-/gulp-debounced-watch-1.0.4.tgz", - "integrity": "sha1-WkfU4kzkY2XOguysMqKjA+QysSo=", + "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, - "requires": { - "debounce-hashed": "^0.1.1", - "gulp-watch": "^4.3.4", - "object-assign": "^3.0.0" + "engines": { + "node": ">=8" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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, "dependencies": { - "gulp-watch": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/gulp-watch/-/gulp-watch-4.3.11.tgz", - "integrity": "sha1-Fi/FY96fx3DpH5p845VVE6mhGMA=", - "dev": true, - "requires": { - "anymatch": "^1.3.0", - "chokidar": "^1.6.1", - "glob-parent": "^3.0.1", - "gulp-util": "^3.0.7", - "object-assign": "^4.1.0", - "path-is-absolute": "^1.0.1", - "readable-stream": "^2.2.2", - "slash": "^1.0.0", - "vinyl": "^1.2.0", - "vinyl-file": "^2.0.0" - }, - "dependencies": { - "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-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "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": { - "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" - } - } + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "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/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, - "requires": { - "multimatch": "^2.0.0", - "plugin-error": "^0.1.2", - "streamfilter": "^1.0.5" + "engines": { + "node": ">=8" } }, - "gulp-gunzip": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulp-gunzip/-/gulp-gunzip-1.0.0.tgz", - "integrity": "sha1-FbdBFF6Dqcb1CIYkG1fMWHHxUak=", + "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, - "requires": { - "through2": "~0.6.5", - "vinyl": "~0.4.6" + "optional": true, + "engines": { + "node": ">=6" + } + }, + "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, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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" + } + }, + "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, + "license": "MIT", "dependencies": { - "clone": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", - "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", - "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 - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "dev": true, - "requires": { - "readable-stream": ">=1.0.33-1 <1.1.0-0", - "xtend": ">=4.0.0 <4.1.0-0" - } + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "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": { + "@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" + } + }, + "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, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/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 + }, + "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.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" }, - "vinyl": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", - "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", - "dev": true, - "requires": { - "clone": "^0.2.0", - "clone-stats": "^0.0.1" - } + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" } + ] + }, + "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, + "dependencies": { + "reusify": "^1.0.4" } }, - "gulp-inline-source": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/gulp-inline-source/-/gulp-inline-source-3.2.0.tgz", - "integrity": "sha512-Ky5SKDgM517QZ6Jw9rKhYz+ynX9T4sr72iUW2fbOhqzN74D37DzWZDZnbKLSwdlTbbqt7JFq/JmUmT12qroW+A==", + "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": { - "inline-source": "~5.2.6", - "plugin-error": "~1.0.1", - "through2": "~2.0.0" + "dependencies": { + "pend": "~1.2.0" + } + }, + "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, + "dependencies": { + "flat-cache": "^3.0.4" }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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, + "license": "MIT", "dependencies": { - "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": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" - } - } + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=6" } }, - "gulp-json-editor": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/gulp-json-editor/-/gulp-json-editor-2.4.1.tgz", - "integrity": "sha512-20nYwO5Bec5X6DfXmBmHEtDAyluTkMguhuvCzqwrHDv/NzwOn3qS4ofAMw9L2gnWAmzxKzHAkFO19LNDWyTwlg==", + "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": { - "deepmerge": "^2.1.0", - "detect-indent": "^5.0.0", - "js-beautify": "^1.7.5", - "plugin-error": "^1.0.1", - "through2": "^2.0.3" + "dependencies": { + "to-regex-range": "^5.0.1" }, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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, "dependencies": { - "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": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" - } - } + "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" } }, - "gulp-remote-src-vscode": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/gulp-remote-src-vscode/-/gulp-remote-src-vscode-0.5.1.tgz", - "integrity": "sha512-mw4OGjtC/jlCWJFhbcAlel4YPvccChlpsl3JceNiB/DLJi24/UPxXt53/N26lgI3dknEqd4ErfdHrO8sJ5bATQ==", + "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": { - "event-stream": "3.3.4", - "node.extend": "^1.1.2", - "request": "^2.79.0", - "through2": "^2.0.3", - "vinyl": "^2.0.1" + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, + "engines": { + "node": ">=8" + } + }, + "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, "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "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 - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" } }, - "gulp-sourcemaps": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-2.6.4.tgz", - "integrity": "sha1-y7IAhFCxvM5s0jv5gze+dRv24wo=", + "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": { - "@gulp-sourcemaps/identity-map": "1.X", - "@gulp-sourcemaps/map-sources": "1.X", - "acorn": "5.X", - "convert-source-map": "1.X", - "css": "2.X", - "debug-fabulous": "1.X", - "detect-newline": "2.X", - "graceful-fs": "4.X", - "source-map": "~0.6.0", - "strip-bom-string": "1.X", - "through2": "2.X" + "dependencies": { + "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" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">= 10.13.0" + } + }, + "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" + } + }, + "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": { - "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 - } + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, - "gulp-symdest": { + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true + }, + "node_modules/flush-write-stream": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/gulp-symdest/-/gulp-symdest-1.1.1.tgz", - "integrity": "sha512-UHd3MokfIN7SrFdsbV5uZTwzBpL0ZSTu7iq98fuDqBGZ0dlHxgbQBJwfd6qjCW83snkQ3Hz9IY4sMRMz2iTq7w==", + "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": { - "event-stream": "3.3.4", - "mkdirp": "^0.5.1", - "queue": "^3.1.0", - "vinyl-fs": "^2.4.3" - }, "dependencies": { - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1" - } - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, - "requires": { - "expand-range": "^1.8.1", - "preserve": "^0.2.0", - "repeat-element": "^1.1.2" - } - }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "^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" - } - }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-stream": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", - "integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=", - "dev": true, - "requires": { - "extend": "^3.0.0", - "glob": "^5.0.3", - "glob-parent": "^3.0.0", - "micromatch": "^2.3.7", - "ordered-read-streams": "^0.3.0", - "through2": "^0.6.0", - "to-absolute-glob": "^0.1.1", - "unique-stream": "^2.0.2" - }, - "dependencies": { - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "dev": true, - "requires": { - "readable-stream": ">=1.0.33-1 <1.1.0-0", - "xtend": ">=4.0.0 <4.1.0-0" - } - } - } - }, - "gulp-sourcemaps": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", - "integrity": "sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=", - "dev": true, - "requires": { - "convert-source-map": "^1.1.1", - "graceful-fs": "^4.1.2", - "strip-bom": "^2.0.0", - "through2": "^2.0.0", - "vinyl": "^1.0.0" - } - }, - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - }, - "is-valid-glob": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz", - "integrity": "sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=", - "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 - }, - "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": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "^2.0.0", - "array-unique": "^0.2.1", - "braces": "^1.8.2", - "expand-brackets": "^0.1.4", - "extglob": "^0.3.1", - "filename-regex": "^2.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.1", - "kind-of": "^3.0.2", - "normalize-path": "^2.0.1", - "object.omit": "^2.0.0", - "parse-glob": "^3.0.4", - "regex-cache": "^0.4.2" - } - }, - "ordered-read-streams": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", - "integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=", - "dev": true, - "requires": { - "is-stream": "^1.0.1", - "readable-stream": "^2.0.1" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - }, - "strip-bom-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", - "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", - "dev": true, - "requires": { - "first-chunk-stream": "^1.0.0", - "strip-bom": "^2.0.0" - } - }, - "to-absolute-glob": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz", - "integrity": "sha1-HN+kcqnvUMI57maZm2YsoOs5k38=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1" - } - }, - "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" - } - }, - "vinyl-fs": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", - "integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=", - "dev": true, - "requires": { - "duplexify": "^3.2.0", - "glob-stream": "^5.3.2", - "graceful-fs": "^4.0.0", - "gulp-sourcemaps": "1.6.0", - "is-valid-glob": "^0.3.0", - "lazystream": "^1.0.0", - "lodash.isequal": "^4.0.0", - "merge-stream": "^1.0.0", - "mkdirp": "^0.5.0", - "object-assign": "^4.0.0", - "readable-stream": "^2.0.4", - "strip-bom": "^2.0.0", - "strip-bom-stream": "^1.0.0", - "through2": "^2.0.0", - "through2-filter": "^2.0.0", - "vali-date": "^1.0.0", - "vinyl": "^1.0.0" - } - } + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" } }, - "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/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": { - "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" - }, + "license": "MIT", "dependencies": { - "clone": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", - "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", - "dev": true - }, - "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 - }, - "glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", - "dev": true, - "requires": { - "extend": "^3.0.0", - "glob": "^7.1.1", - "glob-parent": "^3.1.0", - "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.0", - "pumpify": "^1.3.5", - "readable-stream": "^2.1.5", - "remove-trailing-separator": "^1.0.1", - "to-absolute-glob": "^2.0.0", - "unique-stream": "^2.0.2" - } - }, - "ordered-read-streams": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", - "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - } - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "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" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "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==", - "dev": true - }, - "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" - } - }, - "unique-stream": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", - "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", - "dev": true, - "requires": { - "json-stable-stringify": "^1.0.0", - "through2-filter": "^2.0.0" - } - }, - "vinyl": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", - "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - }, - "vinyl-fs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", - "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", - "dev": true, - "requires": { - "fs-mkdirp-stream": "^1.0.0", - "glob-stream": "^6.1.0", - "graceful-fs": "^4.0.0", - "is-valid-glob": "^1.0.0", - "lazystream": "^1.0.0", - "lead": "^1.0.0", - "object.assign": "^4.0.4", - "pumpify": "^1.3.5", - "readable-stream": "^2.3.3", - "remove-bom-buffer": "^3.0.0", - "remove-bom-stream": "^1.2.0", - "resolve-options": "^1.1.0", - "through2": "^2.0.0", - "to-through": "^2.0.0", - "value-or-function": "^3.0.0", - "vinyl": "^2.0.0", - "vinyl-sourcemap": "^1.1.0" - } - } + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "gulp-untar": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/gulp-untar/-/gulp-untar-0.0.7.tgz", - "integrity": "sha512-0QfbCH2a1k2qkTLWPqTX+QO4qNsHn3kC546YhAP3/n0h+nvtyGITDuDrYBMDZeW4WnFijmkOvBWa5HshTic1tw==", + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "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": { - "event-stream": "~3.3.4", - "streamifier": "~0.1.1", - "tar": "^2.2.1", - "through2": "~2.0.3", - "vinyl": "^1.2.0" - }, "dependencies": { - "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" - } - } + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "gulp-util": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", - "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", - "dev": true, - "requires": { - "array-differ": "^1.0.0", - "array-uniq": "^1.0.2", - "beeper": "^1.0.0", - "chalk": "^1.0.0", - "dateformat": "^2.0.0", - "fancy-log": "^1.1.0", - "gulplog": "^1.0.0", - "has-gulplog": "^0.1.0", - "lodash._reescape": "^3.0.0", - "lodash._reevaluate": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.template": "^3.0.0", - "minimist": "^1.1.0", - "multipipe": "^0.1.2", - "object-assign": "^3.0.0", - "replace-ext": "0.0.1", - "through2": "^2.0.0", - "vinyl": "^0.5.0" + "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, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" }, + "engines": { + "node": ">=8.0.0" + } + }, + "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, + "license": "MIT", "dependencies": { - "object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", - "dev": true - } + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, - "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/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": { - "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" + "engines": { + "node": ">=8" + } + }, + "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, + "dependencies": { + "shebang-regex": "^3.0.0" }, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "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 - }, - "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" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } + "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" } }, - "gulp-watch": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/gulp-watch/-/gulp-watch-5.0.0.tgz", - "integrity": "sha512-q+HLppxXd11z9ndqql4Z0sd5xOAesJjycl0PRaq6ImK7b1BqBRL37YvxEE8ngUdIfpfHa0O9OCoovoggcFpCaQ==", + "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": { - "anymatch": "^1.3.0", - "chokidar": "^2.0.0", - "glob-parent": "^3.0.1", - "gulp-util": "^3.0.7", - "object-assign": "^4.1.0", - "path-is-absolute": "^1.0.1", - "readable-stream": "^2.2.2", - "slash": "^1.0.0", - "vinyl": "^2.1.0", - "vinyl-file": "^2.0.0" + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "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 + }, + "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 + }, + "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": { - "chokidar": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz", - "integrity": "sha512-zW8iXYZtXMx4kux/nuZVXjkLP+CyIK5Al5FHnj1OgTKGZfp4Oy6/ymtMSKFv3GD8DviEmUPmJg9eFdJ/JzudMg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.1.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^2.1.1", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.0" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - } - } - }, - "clone": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", - "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", - "dev": true - }, - "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 - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "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" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "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": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", - "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "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/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" } }, - "gzip-size": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", - "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=", + "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": { - "duplexer": "^0.1.1" + "dependencies": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" } }, - "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "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": { - "async": "^1.4.0", - "optimist": "^0.6.1", - "source-map": "^0.4.4", - "uglify-js": "^2.6" - }, "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } + "async": "*" } }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "^5.1.0", - "har-schema": "^2.0.0" + "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, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "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": { - "function-bind": "^1.1.1" + "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" } }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "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": { - "ansi-regex": "^2.0.0" + "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" } }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "has-gulplog": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", - "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", + "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, - "requires": { - "sparkles": "^1.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "has-symbol-support-x": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", - "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", - "dev": true - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true + "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, + "engines": { + "node": ">=6.9.0" + } }, - "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/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, - "requires": { - "has-symbol-support-x": "^1.4.1" + "engines": { + "node": "6.* || 8.* || >= 10.*" } }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" + "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": "*" } }, - "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=", - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.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": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "^1.1.5" - } - } + "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" } }, - "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/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, + "engines": { + "node": ">=8.0.0" } }, - "hash.js": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", - "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", + "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": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, - "hmac-drbg": { + "node_modules/get-proto": { "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" + "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" } }, - "hoist-non-react-statics": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", - "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==", - "dev": true - }, - "homedir-polyfill": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", - "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "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, - "requires": { - "parse-passwd": "^1.0.0" + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "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.6.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", - "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", - "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" + } }, - "html-encoding-sniffer": { + "node_modules/get-symbol-description": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "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": { - "whatwg-encoding": "^1.0.1" + "dependencies": { + "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" } }, - "html-minifier": { - "version": "3.5.20", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.20.tgz", - "integrity": "sha512-ZmgNLaTp54+HFKkONyLFEfs5dd/ZOtlquKaTnqIWFmx3Av5zG6ZPcV2d0o9XM2fXOTxxIf6eDcwzFFotke/5zA==", + "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": { - "camel-case": "3.0.x", - "clean-css": "4.2.x", - "commander": "2.17.x", - "he": "1.1.x", - "param-case": "2.1.x", - "relateurl": "0.2.x", - "uglify-js": "3.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": { - "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 - }, - "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 - }, - "uglify-js": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", - "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", - "dev": true, - "requires": { - "commander": "~2.17.1", - "source-map": "~0.6.1" - } - } + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" } }, - "html-webpack-plugin": { - "version": "3.2.0", - "resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", - "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", + "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": { - "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": { - "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" - } - } + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "htmlparser2": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", - "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", + "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, - "requires": { - "domelementtype": "^1.3.0", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^2.0.2" + "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" } }, - "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==", + "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 }, - "http-errors": { - "version": "1.6.3", - "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "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": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "dependencies": { + "async-done": "^2.0.0", + "chokidar": "^3.5.3" + }, + "engines": { + "node": ">= 10.13.0" } }, - "http-parser-js": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz", - "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=", - "dev": true + "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": "*" + } }, - "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/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "httpplease": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/httpplease/-/httpplease-0.16.4.tgz", - "integrity": "sha1-04Lr4jDvUHkIC06f/r8xap51wNo=", - "requires": { - "urllite": "~0.5.0", - "xmlhttprequest": "*", - "xtend": "~3.0.0" + "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, + "dependencies": { + "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" + } + }, + "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, "dependencies": { - "xtend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz", - "integrity": "sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=" - } + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, - "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/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, + "engines": { + "node": ">=4" + } }, - "husky": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/husky/-/husky-1.1.2.tgz", - "integrity": "sha512-9TdkUpBeEOjz0AnFdUN4i3w8kEbOsVs9/WSeJqWLq2OO6bcKQhVW64Zi+pVd/AMRLpN3QTINb6ZXiELczvdmqQ==", + "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, - "requires": { - "cosmiconfig": "^5.0.6", - "execa": "^0.9.0", - "find-up": "^3.0.0", - "get-stdin": "^6.0.0", - "is-ci": "^1.2.1", - "pkg-dir": "^3.0.0", - "please-upgrade-node": "^3.1.1", - "read-pkg": "^4.0.1", - "run-node": "^1.0.0", - "slash": "^2.0.0" + "dependencies": { + "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": { - "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" - } - }, - "execa": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.9.0.tgz", - "integrity": "sha512-BbUMBiX4hqiHZUA5+JujIjNb6TyAlp2D5KLheMjMluwOuzcnylDL4AxZYLLn1n2AGB49eSWwyKvvEQoRpnAtmA==", - "dev": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.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" - } - }, - "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" - } - }, - "get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true - }, - "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" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "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 - }, - "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" - } - }, - "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" - } - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "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" } }, - "iconv-lite": { - "version": "0.4.21", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", - "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", - "requires": { - "safer-buffer": "^2.1.0" + "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, + "dependencies": { + "sparkles": "^2.1.0" + }, + "engines": { + "node": ">= 10.13.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/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" + } }, - "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/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" + "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" } }, - "ieee754": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.11.tgz", - "integrity": "sha512-VhDzCKN7K8ufStx/CLj5/PDTMgph+qwN5Pkd5i0sGnVwk56zJ0lkT8Qzi1xqWLS0Wp29DgDtNeS7v8/wMoZeHg==", - "dev": true + "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" + } }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", - "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" + } }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "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==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "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/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": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - }, "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" - } - }, - "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 - }, - "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" - } - } + "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" } }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true + "node_modules/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, + "dependencies": { + "@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" + } }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true + "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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" + "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, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "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 }, - "inline-source": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/inline-source/-/inline-source-5.2.7.tgz", - "integrity": "sha512-RvMOGMXxAqqve4ld128B7TYyNR2aP1LB38dcSpWFmqXrhKPuey1+yFU6kFUgyH8IWX+gRZdWtHN4eQ9d0IpFZg==", + "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" + } + }, + "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, - "requires": { - "csso": "3.4.x", - "htmlparser2": "3.9.x", - "is-plain-obj": "1.1.x", - "object-assign": "4.1.x", - "svgo": "0.7.x", - "uglify-js": "3.3.x" - }, "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 - }, - "uglify-js": { - "version": "3.3.28", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.28.tgz", - "integrity": "sha512-68Rc/aA6cswiaQ5SrE979UJcXX+ADA1z33/ZsPd+fbAiVdjZ16OXdbtGO+rJUUBgK6qdf3SOPhQf3K/ybF5Miw==", - "dev": true, - "requires": { - "commander": "~2.15.0", - "source-map": "~0.6.1" - } - } + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "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" + "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" + } + }, + "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, + "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" + } + }, + "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-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "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" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - }, - "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" - } - } + "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" } }, - "interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", - "dev": true - }, - "into-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", - "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "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, - "requires": { - "from2": "^2.1.1", - "p-is-promise": "^1.1.0" + "engines": { + "node": ">=6" } }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "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, - "requires": { - "loose-envify": "^1.0.0" + "engines": { + "node": ">= 8" } }, - "inversify": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/inversify/-/inversify-4.11.1.tgz", - "integrity": "sha512-9bs/36crPdTSOCcoomHMb96s+B8W0+2c9dHFP/Srv9ZQaPnUvsMgzmMHfgVECqfHVUIW+M5S7SYOjoig8khWuQ==" + "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" + } }, - "invert-kv": { + "node_modules/gulp/node_modules/convert-source-map": { "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==", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, - "ipaddr.js": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", - "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=", - "dev": true - }, - "is": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", - "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU=", - "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==", + "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": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" + "dependencies": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + }, + "engines": { + "node": ">=10.13.0" } }, - "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=", - "requires": { - "kind-of": "^3.0.2" - }, + "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": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" } }, - "is-alphabetical": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.2.tgz", - "integrity": "sha512-V0xN4BYezDHcBSKb1QHUFMlR4as/XEuCZBzMJUU4n7+Cbt33SmUnSol+pnXFvLxSHNq2CemUXNdaXV6Flg7+xg==", - "dev": true - }, - "is-alphanumerical": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.2.tgz", - "integrity": "sha512-pyfU/0kHdISIgslFfZN9nfY1Gk3MquQgUm1mJTjdkEPpkAKNWuBTSqFwewOpR7N351VkErCiyV71zX7mlQQqsg==", + "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, - "requires": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" + "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" } }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "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=", + "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": { - "binary-extensions": "^1.0.0" + "engines": { + "node": ">=10.13.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 + "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, + "dependencies": { + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.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==" + "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" + } }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "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, - "requires": { - "builtin-modules": "^1.0.0" + "dependencies": { + "value-or-function": "^4.0.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", - "dev": true + "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, + "dependencies": { + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "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": { - "ci-info": "^1.5.0" + "engines": { + "node": ">= 10.13.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=", - "requires": { - "kind-of": "^3.0.2" - }, + "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, "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } + "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" } }, - "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.2", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.2.tgz", - "integrity": "sha512-TRzl7mOCchnhchN+f3ICUCzYvL9ul7R+TYOsZ8xia++knyZAJfv/uA1FvQXsAnYIl1T3B2X5E/J7Wb1QXiIBXg==", - "dev": true + "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, + "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" + } }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "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" + } + }, + "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": { - "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==" - } + "glogg": "^2.2.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "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 + "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" + } }, - "is-dotfile": { + "node_modules/has": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, - "requires": { - "is-primitive": "^2.0.0" + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" } }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "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 + "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, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "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": { - "is-extglob": "^2.1.0" + "engines": { + "node": ">=4" } }, - "is-hexadecimal": { + "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz", - "integrity": "sha512-but/G3sapV3MNyqiDBLrOi4x8uCIw0RY3o/Vb5GT0sMFHrVV7731wFSVy41T5FO1og7G0gXLJh0MkgPRouko/A==", - "dev": true - }, - "is-natural-number": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", - "dev": true - }, - "is-negated-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "is-number-object": { + "node_modules/has-proto": { "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 + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "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": "*" + } }, - "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 + "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" + } }, - "is-odd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", - "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", - "requires": { - "is-number": "^4.0.0" + "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": { + "has-symbol-support-x": "^1.4.1" }, + "engines": { + "node": "*" + } + }, + "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": { - "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==" - } + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true + "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, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": ">=4" + } }, - "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==", + "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-path-inside": "^1.0.0" + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" } }, - "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=", + "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, - "requires": { - "path-is-inside": "^1.0.1" + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" } }, - "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==", - "requires": { - "isobject": "^3.0.1" - } - }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { + "node_modules/hasha/node_modules/is-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true + "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, + "engines": { + "node": ">=8" + } }, - "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 + "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" + } }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "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, - "requires": { - "has": "^1.0.1" + "bin": { + "he": "bin/he" } }, - "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==", + "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": { - "is-unc-path": "^1.0.0" + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" } }, - "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 + "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" + } }, - "is-root": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-1.0.0.tgz", - "integrity": "sha1-B7bCM7w5TNnQK6FclmvWZg1jQtU=", + "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 }, - "is-running": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-running/-/is-running-2.1.0.tgz", - "integrity": "sha1-MKc/9cw4VOT8JUkICen1q/jeCeA=", - "dev": true + "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, + "license": "BSD-2-Clause" }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + "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" + } }, - "is-string": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.4.tgz", - "integrity": "sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=", - "dev": true + "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, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } }, - "is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", + "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 }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, - "requires": { - "has-symbols": "^1.0.0" + "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" } }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "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": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } }, - "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==", + "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": { - "unc-path-regex": "^0.1.2" + "engines": { + "node": ">=10.17.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 - }, - "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 + "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" + } }, - "is-whitespace-character": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.2.tgz", - "integrity": "sha512-SzM+T5GKUCtLhlHFKt2SDAX2RFzfS6joT91F2/WSi9LxgFdsnhfPK/UIA+JhRR2xuyLdrCys2PiFDrtn1fU5hQ==", - "dev": true + "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, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, - "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==" + "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, + "engines": { + "node": ">= 4" + } }, - "is-word-character": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.2.tgz", - "integrity": "sha512-T3FlsX8rCHAH8e7RE7PfOPZVFQlcV3XRF9eOOBQ1uf70OxO7CjjSOjeImMPCADBdYWcStAbVbYvJ1m2D3tb+EA==", + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "dev": true }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + "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, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "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": { + "acorn": "^8.8.2", + "acorn-import-assertions": "^1.9.0", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "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, + "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" + } }, - "isomorphic-fetch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "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": { - "node-fetch": "^1.0.1", - "whatwg-fetch": ">=0.10.0" + "engines": { + "node": ">=0.8.19" } }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "istanbul": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", - "dev": true, - "requires": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "abbrev": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", - "dev": true - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } + "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, + "engines": { + "node": ">=8" } }, - "istanbul-lib-coverage": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", - "integrity": "sha512-nPvSZsVlbG9aLhZYaC3Oi1gT/tpyo3Yt5fNyf6NmcKIayz4VV/txxJFFKAK/gU4dcNn8ehsanBbVHVl0+amOLA==", + "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" + } + }, + "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 }, - "istanbul-lib-instrument": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.0.0.tgz", - "integrity": "sha512-eQY9vN9elYjdgN9Iv6NS/00bptm02EBBk70lRMaVjeA6QYocQgenVrSgC28TJurdnZa80AGO3ASdFN+w/njGiQ==", + "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, - "requires": { - "@babel/generator": "^7.0.0", - "@babel/parser": "^7.0.0", - "@babel/template": "^7.0.0", - "@babel/traverse": "^7.0.0", - "@babel/types": "^7.0.0", - "istanbul-lib-coverage": "^2.0.1", - "semver": "^5.5.0" + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" } }, - "isurl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", - "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "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, - "requires": { - "has-to-string-tag-x": "^1.2.0", - "is-object": "^1.0.1" + "engines": { + "node": ">=10.13.0" } }, - "js-beautify": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.7.5.tgz", - "integrity": "sha512-9OhfAqGOrD7hoQBLJMTA+BKuKmoEtTJXzZ7WDF/9gvjtey1koVLuZqIY6c51aPDjbNdNtIXAkiWKVhziawE9Og==", + "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": { - "config-chain": "~1.1.5", - "editorconfig": "^0.13.2", - "mkdirp": "~0.5.0", - "nopt": "~3.0.1" + "license": "MIT", + "dependencies": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + }, + "engines": { + "node": ">=4" } }, - "js-levenshtein": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.4.tgz", - "integrity": "sha512-PxfGzSs0ztShKrUYPIn5r0MtyAhYcCwmndozzpz8YObbPnD1jFxzlBGbRnX2mIu6Z13xN6+PTu05TQFnZFlzow==", - "dev": true + "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==" }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + "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" + } }, - "js-yaml": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", - "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", + "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": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "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-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": { - "esprima": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", - "dev": true - } + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "jsdom": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-12.2.0.tgz", - "integrity": "sha512-QPOggIJ8fquWPLaYYMoh+zqUmdphDtu1ju0QGTitZT1Yd8I5qenPpXM1etzUegu3MjVp8XPzgZxdn8Yj7e40ig==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "acorn": "^6.0.2", - "acorn-globals": "^4.3.0", - "array-equal": "^1.0.0", - "cssom": "^0.3.4", - "cssstyle": "^1.1.1", - "data-urls": "^1.0.1", - "domexception": "^1.0.1", - "escodegen": "^1.11.0", - "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.0.9", - "parse5": "5.1.0", - "pn": "^1.1.0", - "request": "^2.88.0", - "request-promise-native": "^1.0.5", - "saxes": "^3.1.3", - "symbol-tree": "^3.2.2", - "tough-cookie": "^2.4.3", - "w3c-hr-time": "^1.0.1", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0", - "ws": "^6.1.0", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.2.tgz", - "integrity": "sha512-GXmKIvbrN3TV7aVqAzVFaMW8F8wzVX7voEBRO3bDA64+EX37YSayggRJP5Xig6HYHBkWKpFg9W5gg6orklubhg==", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "escodegen": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz", - "integrity": "sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==", - "dev": true, - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "har-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", - "dev": true, - "requires": { - "ajv": "^5.3.0", - "har-schema": "^2.0.0" - } - }, - "mime-db": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", - "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==", - "dev": true - }, - "mime-types": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", - "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", - "dev": true, - "requires": { - "mime-db": "~1.36.0" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", - "dev": true - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "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" - } - }, - "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, - "optional": true - }, - "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==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true - }, - "ws": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.0.tgz", - "integrity": "sha512-H3dGVdGvW2H8bnYpIDc3u3LH8Wue3Qh+Zto6aXXFzvESkTVT6rAfKR6tR/+coaUvxs8yHtmNV0uioBF62ZGSTg==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } - } + "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" } }, - "jsesc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz", - "integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=", - "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=", - "dev": true - }, - "json-edm-parser": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/json-edm-parser/-/json-edm-parser-0.1.2.tgz", - "integrity": "sha1-HmCw/vG8CvZ7wNFG393lSGzWFbQ=", - "requires": { - "jsonparse": "~1.2.0" + "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" } }, - "json-loader": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", - "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", - "dev": true + "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" + } }, - "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==", + "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 }, - "json-parser": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/json-parser/-/json-parser-1.1.5.tgz", - "integrity": "sha1-5i7FJh0aal/CDoEqMgdAxtkAVnc=", - "requires": { - "esprima": "^2.7.0" + "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" } }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + "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" + } }, - "json-stable-stringify": { + "node_modules/is-data-view": { "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=", - "requires": { - "jsonify": "~0.0.0" + "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" } }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "json2csv": { - "version": "3.11.5", - "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-3.11.5.tgz", - "integrity": "sha512-ORsw84BuRKMLxfI+HFZuvxRDnsJps53D5fIGr6tLn4ZY+ymcG8XU00E+JJ2wfAiHx5w2QRNmOLE8xHiGAeSfuQ==", - "dev": true, - "requires": { - "cli-table": "^0.3.1", - "commander": "^2.8.1", - "debug": "^3.1.0", - "flat": "^4.0.0", - "lodash.clonedeep": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.get": "^4.4.0", - "lodash.set": "^4.3.0", - "lodash.uniq": "^4.5.0", - "path-is-absolute": "^1.0.0" - }, + "node_modules/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, "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" - } - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - } + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "json3": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", - "dev": true - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", - "dev": true - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" + "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" } }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" - }, - "jsonparse": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz", - "integrity": "sha1-XAxWhRBxYOcv50ib3eoLRMK8Z70=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" + "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" } }, - "just-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", - "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=", - "dev": true - }, - "keyv": { + "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", - "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "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, - "requires": { - "json-buffer": "3.0.0" + "engines": { + "node": ">=8" } }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "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" + } }, - "labella": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/labella/-/labella-1.1.4.tgz", - "integrity": "sha1-xsxaNA6N80DrM1YzaD6lm4KMMi0=", - "dev": true + "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" + } }, - "last-run": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", - "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", + "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, - "requires": { - "default-resolution": "^2.0.0", - "es6-weak-map": "^2.0.1" + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "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, - "optional": true + "license": "MIT" }, - "lazystream": { + "node_modules/is-negated-glob": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", "dev": true, - "requires": { - "readable-stream": "^2.0.5" + "engines": { + "node": ">=0.10.0" } }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "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, - "requires": { - "invert-kv": "^2.0.0" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "lead": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", - "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", + "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, - "requires": { - "flush-write-stream": "^1.0.2" + "engines": { + "node": ">=0.12.0" } }, - "leaflet": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.3.4.tgz", - "integrity": "sha512-FYL1LGFdj6v+2Ifpw+AcFIuIOqjNggfoLUwuwQv6+3sS21Za7Wvapq+LhbSE4NDXrEj6eYnW3y7LsaBICpyXtw==", - "dev": true + "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" + } }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "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, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "liftoff": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", - "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", + "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, - "requires": { - "extend": "^3.0.0", - "findup-sync": "^2.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" + "engines": { + "node": ">=6" } }, - "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==" + "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" + } }, - "load-json-file": { + "node_modules/is-plain-obj": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - } + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "loader-runner": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.1.tgz", - "integrity": "sha512-By6ZFY7ETWOc9RFaAIb23IjJVcM4dvJC/N57nmdz9RSkMXvAXGI7SyVlAw3v8vjtDRlqThgVDVmTnr9fqMlxkw==", - "dev": true - }, - "loader-utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", - "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "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, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0" + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "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, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "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": { - "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" - } - }, - "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 - } + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" - }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash._basetostring": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", - "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", - "dev": true - }, - "lodash._basevalues": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", - "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=", - "dev": true - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true - }, - "lodash._reescape": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", - "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=", - "dev": true - }, - "lodash._reevaluate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", - "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=", - "dev": true - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true - }, - "lodash._root": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", - "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", - "dev": true + "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" + } }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true + "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" + } }, - "lodash.curry": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", - "integrity": "sha1-JI42By7ekGUB11lmIAqG2riyMXA=", - "dev": true + "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" + } }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true + "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" + } }, - "lodash.escape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", - "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", + "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, - "requires": { - "lodash._root": "^3.0.0" + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", - "dev": true + "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" + } }, - "lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "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 }, - "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 + "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" + } }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "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 }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "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" + } }, - "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 + "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": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "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": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" + "engines": { + "node": ">=0.10.0" } }, - "lodash.restparam": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", - "dev": true + "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, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } }, - "lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, - "lodash.template": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", - "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true, - "requires": { - "lodash._basecopy": "^3.0.0", - "lodash._basetostring": "^3.0.0", - "lodash._basevalues": "^3.0.0", - "lodash._isiterateecall": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0", - "lodash.keys": "^3.0.0", - "lodash.restparam": "^3.0.0", - "lodash.templatesettings": "^3.0.0" + "engines": { + "node": ">=0.10.0" } }, - "lodash.templatesettings": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", - "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", + "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": { - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0" + "engines": { + "node": ">=8" } }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "log-symbols": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", - "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "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": { - "chalk": "^2.0.1" - }, "dependencies": { - "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" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - } + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, - "loglevelnext": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/loglevelnext/-/loglevelnext-1.0.5.tgz", - "integrity": "sha512-V/73qkPuJmx4BcBF19xPBr+0ZRVBhc4POxvZTZdMeXpJ4NItXSJ/MSwuFT0kQJlCbXvdlZoQQ/418bS1y9Jh6A==", + "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, - "requires": { - "es6-symbol": "^3.1.1", - "object.assign": "^4.1.0" + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" } }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" + "node_modules/istanbul-lib-instrument/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" } }, - "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==", - "dev": true - }, - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "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": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "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" } }, - "lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", + "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": { - "es5-ext": "~0.10.2" + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "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, - "requires": { - "pify": "^3.0.0" + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "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": { - "kind-of": "^6.0.2" + "engines": { + "node": ">=8" } }, - "map-age-cleaner": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.2.tgz", - "integrity": "sha512-UN1dNocxQq44IhJyMI4TU8phc2m9BddacHRPRjKGLYaF0jqd3xLz0jS0skpAU9WgYyoR4gHtUpzytNBS385FWQ==", + "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": { - "p-defer": "^1.0.0" + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" - }, - "map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "^1.0.0" + "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, + "engines": { + "node": ">=8" } }, - "markdown-escapes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.2.tgz", - "integrity": "sha512-lbRZ2mE3Q9RtLjxZBZ9+IMl68DKIXaVAhwvwn9pmjnPLS0h/6kyBMgNhqi1xFJ/2yv6cSyv0jbiZavZv93JkkA==", - "dev": true + "node_modules/istanbul-lib-processinfo/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==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } }, - "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=", + "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": { - "bintrees": "^1.0.1", - "tinyqueue": "^1.1.0" + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" } }, - "matchdep": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", - "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", + "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": { - "findup-sync": "^2.0.0", - "micromatch": "^3.0.4", - "resolve": "^1.4.0", - "stack-trace": "0.0.10" + "engines": { + "node": ">=8" } }, - "material-colors": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", - "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==", - "dev": true + "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" + } }, - "math-random": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", - "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", - "dev": true - }, - "md5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", - "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", - "requires": { - "charenc": "~0.0.1", - "crypt": "~0.0.1", - "is-buffer": "~1.1.1" + "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, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8" } }, - "md5.js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", - "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "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, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "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==", + "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": { - "unist-util-visit-parents": "1.1.2" + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "mdn-data": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-1.1.4.tgz", - "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==", - "dev": true - }, - "media-typer": { - "version": "0.3.0", - "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true - }, - "mem": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.0.0.tgz", - "integrity": "sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA==", + "node_modules/isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^1.0.0", - "p-is-promise": "^1.1.0" + "license": "MIT", + "dependencies": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + }, + "engines": { + "node": ">= 4" } }, - "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 - }, - "memoizee": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.12.tgz", - "integrity": "sha512-sprBu6nwxBWBvBOh5v2jcsGqiGLlL2xr2dLub3vR8dnE8YB17omwtm/0NSHl8jjNbcsJd5GMWJAnTSVe/O0Wfg==", + "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": { - "d": "1", - "es5-ext": "^0.10.30", - "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.2" + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.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=", + "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": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" } }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true + "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, + "engines": { + "node": ">=8" + } }, - "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", + "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": { - "readable-stream": "^2.0.1" + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "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 }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "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" + "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, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "miller-rabin": { + "node_modules/js-yaml/node_modules/esprima": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" } }, - "mime": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", - "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==", - "dev": true - }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "requires": { - "mime-db": "~1.33.0" + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" } }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "mimic-response": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.0.tgz", - "integrity": "sha1-3z02Uqc/3ta5sLJBRub9BSNTRY4=", - "dev": true - }, - "min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", + "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": { - "dom-walk": "^0.1.0" - } + "license": "MIT" }, - "mini-svg-data-uri": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.0.2.tgz", - "integrity": "sha512-3bDQR0/DIws7pkqi/dhtmv5BGgTT2HPRzq9fos3Jz4Xc9bVnn5eC6jBb4mK25Jdt8UclKeRhateLLTz9J2Wwug==" + "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 }, - "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==", + "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 }, - "minimalistic-crypto-utils": { + "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "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 }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" + "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, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "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==" }, - "mississippi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz", - "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==", + "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": { - "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": "^2.0.1", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - }, "dependencies": { - "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==", - "dev": true, - "requires": { - "once": "^1.4.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" + }, + "engines": { + "node": ">=12", + "npm": ">=6" } }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, + "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, "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==", - "requires": { - "is-plain-object": "^2.0.4" - } - } + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "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": { - "minimist": "0.0.8" - }, "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" } }, - "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "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": { - "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.5", - "he": "1.1.1", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "supports-color": "5.4.0" - }, "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" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } + "array-includes": "^3.1.3", + "object.assign": "^4.1.2" + }, + "engines": { + "node": ">=4.0" } }, - "mocha-junit-reporter": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-1.17.0.tgz", - "integrity": "sha1-LlFJ7UD8XS48px5C21qx/snG2Fw=", + "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": { - "debug": "^2.2.0", - "md5": "^2.1.0", - "mkdirp": "~0.5.1", - "strip-ansi": "^4.0.0", - "xml": "^1.0.0" - }, "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" } }, - "moment": { - "version": "2.21.0", - "resolved": "http://registry.npmjs.org/moment/-/moment-2.21.0.tgz", - "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ==" - }, - "moo": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz", - "integrity": "sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw==", + "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 }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "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": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "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, + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } }, - "multimatch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", - "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, - "requires": { - "array-differ": "^1.0.0", - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "minimatch": "^3.0.0" + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" } }, - "multipipe": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", - "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", + "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": { - "duplexer2": "0.0.2" + "license": "MIT", + "dependencies": { + "json-buffer": "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 + "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" + } }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "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 }, - "named-js-regexp": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/named-js-regexp/-/named-js-regexp-1.3.3.tgz", - "integrity": "sha512-zIUAXzGQOp16VR0Ct89SDstU62hzAPBluNUrUrsdD7MNSRbm/vyqGhEnp+4hnsMjmX3C2wh1cbIEP0joKMFLxw==" - }, - "nan": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", - "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", + "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, - "optional": true - }, - "nanomatch": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", - "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", - "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-odd": "^2.0.0", - "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" + "dependencies": { + "language-subtag-registry": "~0.3.2" } }, - "nearley": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.15.1.tgz", - "integrity": "sha512-8IUY/rUrKz2mIynUGh8k+tul1awMKEjeHHC5G3FHvvyAW6oq4mQfNp2c0BMea+sYZJvYcrrM6GmZVIle/GRXGw==", + "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": { - "moo": "^0.4.3", - "nomnom": "~1.6.2", - "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6", - "semver": "^5.4.1" + "engines": { + "node": ">= 10.13.0" } }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", - "dev": true - }, - "neo-async": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.2.tgz", - "integrity": "sha512-vdqTKI9GBIYcAEbFAcpKPErKINfPF5zIuz3/niBfq8WUZjpT2tytLlFVrBgWdOtqI4uaA/Rb6No0hux39XXDuw==", - "dev": true - }, - "next-tick": { + "node_modules/lazystream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } }, - "nice-try": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.4.tgz", - "integrity": "sha512-2NpiFHqC87y/zFke0fC0spBXL3bBsoh/p5H1EFhshxjCR5+0g2d6BiXbUFz9v1sAcxsk2htp2eQnNIci2dIYcA==", - "dev": true + "node_modules/lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", + "dev": true, + "dependencies": { + "flush-write-stream": "^1.0.2" + }, + "engines": { + "node": ">= 0.10" + } }, - "no-case": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "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": { - "lower-case": "^1.1.1" + "engines": { + "node": ">=6" } }, - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" + "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, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "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=", + "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": { - "fs-walk": "0.0.1" + "dependencies": { + "immediate": "~3.0.5" } }, - "node-libs-browser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", - "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", + "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": { - "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": "^1.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.0", - "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.10.3", - "vm-browserify": "0.0.4" - }, "dependencies": { - "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==", - "dev": true - }, - "buffer": { - "version": "4.9.1", - "resolved": "http://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.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://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" - } - } + "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" } }, - "node-releases": { - "version": "1.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.0.0-alpha.12.tgz", - "integrity": "sha512-VPB4rTPqpVyWKBHbSa4YPFme3+8WHsOSpvbp0Mfj0bWsC8TEjt4HQrLl1hsBDELlp1nB4lflSgSuGTYiuyaP7Q==", + "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": { - "semver": "^5.3.0" + "engines": { + "node": ">=0.10.0" } }, - "node-stream-zip": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.6.0.tgz", - "integrity": "sha512-py/b/mLnyp/VvHCAl/Pqn6y+oLJrWpLYpLxJmGEAs1vxYDoAxgdbOzYgjpjEju/jrHzxUPurF+kT6KTfb+a4tA==" + "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": { + "uc.micro": "^1.0.1" + } }, - "node.extend": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-1.1.8.tgz", - "integrity": "sha512-L/dvEBwyg3UowwqOUTyDsGBU6kjBQOpOhshio9V3i3BMPv5YUb9+mWNN8MK0IbWqT0AqaTSONZf0aTuMMahWgA==", + "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, - "requires": { - "has": "^1.0.3", - "is": "^3.2.1" + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "nomnom": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.6.2.tgz", - "integrity": "sha1-hKZqJgF0QI/Ft3oY+IjszET7aXE=", + "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": { - "colors": "0.5.x", - "underscore": "~1.4.4" + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" }, + "engines": { + "node": ">=8.9.0" + } + }, + "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, "dependencies": { - "colors": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz", - "integrity": "sha1-fQAj6usVTo7p/Oddy5I9DtFmd3Q=", - "dev": true - }, - "underscore": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", - "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=", - "dev": true - } + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "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==" + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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, - "requires": { - "abbrev": "1" + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "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": { - "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "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": { - "remove-trailing-separator": "^1.0.1" + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "normalize-url": { + "node_modules/log-symbols/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", - "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "requires": { - "prepend-http": "^2.0.0", - "query-string": "^5.0.1", - "sort-keys": "^2.0.0" - }, "dependencies": { - "sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", - "dev": true, - "requires": { - "is-plain-obj": "^1.0.0" - } - } + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "now-and-later": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.0.tgz", - "integrity": "sha1-vGHLtFbXnLMiB85HygUTb/Ln1u4=", + "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 + }, + "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": { - "once": "^1.3.2" + "engines": { + "node": ">=8" } }, - "npm-conf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", - "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "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": { - "config-chain": "^1.1.11", - "pify": "^3.0.0" + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "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=", + "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, - "requires": { - "path-key": "^2.0.0" + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" } }, - "nth-check": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", - "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", + "node_modules/loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", "dev": true, - "requires": { - "boolbase": "~1.0.0" + "dependencies": { + "get-func-name": "^2.0.0" } }, - "number-is-nan": { + "node_modules/lowercase-keys": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "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" + } }, - "numeral": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", - "integrity": "sha1-StCAk21EPCVhrtnyGX7//iX05QY=", - "dev": true + "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": { + "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" + } }, - "nwsapi": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.0.9.tgz", - "integrity": "sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ==", + "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 }, - "nyc": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-13.1.0.tgz", - "integrity": "sha512-3GyY6TpQ58z9Frpv4GMExE1SV2tAgYqC7HSy2omEhNiCT3mhT9NyiOvIE8zkbuJVFzmvvNTnE4h/7/wQae7xLg==", + "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, - "requires": { - "archy": "^1.0.0", - "arrify": "^1.0.1", - "caching-transform": "^2.0.0", - "convert-source-map": "^1.6.0", - "debug-log": "^1.0.1", - "find-cache-dir": "^2.0.0", - "find-up": "^3.0.0", - "foreground-child": "^1.5.6", - "glob": "^7.1.3", - "istanbul-lib-coverage": "^2.0.1", - "istanbul-lib-hook": "^2.0.1", - "istanbul-lib-instrument": "^3.0.0", - "istanbul-lib-report": "^2.0.2", - "istanbul-lib-source-maps": "^2.0.1", - "istanbul-reports": "^2.0.1", - "make-dir": "^1.3.0", - "merge-source-map": "^1.1.0", - "resolve-from": "^4.0.0", - "rimraf": "^2.6.2", - "signal-exit": "^3.0.2", - "spawn-wrap": "^1.4.2", - "test-exclude": "^5.0.0", - "uuid": "^3.3.2", - "yargs": "11.1.0", - "yargs-parser": "^9.0.2" - }, "dependencies": { - "align-text": { - "version": "0.1.4", - "bundled": true, - "dev": true, - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - } - }, - "amdefine": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "ansi-regex": { + "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 + } + } + }, + "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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "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 + }, + "esbuild": { + "optional": 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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "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" + } + }, + "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, + "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": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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" + } + }, + "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, + "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" + } + }, + "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 + }, + "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" + } + }, + "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, + "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": { + "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 + }, + "@swc/wasm": { + "optional": true + } + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "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 + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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, + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "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, + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "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, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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, + "engines": { + "node": ">=4" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "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" + } + }, + "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, + "dependencies": { + "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" + } + }, + "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, + "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" + } + }, + "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, + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "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, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "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, + "hasInstallScript": true, + "dependencies": { + "circular-json": "^0.3.1", + "lodash": "^4.17.4", + "postinstall-build": "^5.0.1" + }, + "engines": { + "node": ">=6.0.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, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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, + "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" + } + }, + "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, + "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": { + "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" + } + }, + "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 + }, + "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, + "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": { + "inherits": "2.0.3" + } + }, + "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": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist-node/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, + "engines": { + "node": ">= 10.13.0" + } + }, + "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, + "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": { + "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" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "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, + "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": { + "balanced-match": "^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" + } + }, + "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" + } + }, + "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==" + }, + "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" + } + }, + "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 + } + } + }, + "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, + "dependencies": { + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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 + }, + "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, + "engines": { + "node": ">= 10" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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, + "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": { + "@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 + } + } + }, + "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" + } + }, + "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" + } + }, + "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, + "dependencies": { + "resolve": "^1.9.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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 + }, + "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, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "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, + "peerDependencies": { + "tapable": "^2.2.0" + } + }, + "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" + } + }, + "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, + "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/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, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "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 + }, + "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, + "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": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "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": { + "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" + } + }, + "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, + "dependencies": { + "wipe-node-cache": "^2.1.0" + } + }, + "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, + "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/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "dev": true, + "license": "Apache-2.0" + }, + "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, + "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" + } + }, + "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" + } + }, + "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, + "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": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "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, + "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" + } + }, + "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" + } + }, + "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 + }, + "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, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "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, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "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" + } + }, + "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" + } + }, + "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, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "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==" + }, + "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" + } + }, + "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, + "engines": { + "node": ">=10" + } + }, + "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, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "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" + } + }, + "node_modules/yargs-unparser/node_modules/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, + "engines": { + "node": ">=8" + } + }, + "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, + "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" + } + }, + "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, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "@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": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@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==" + } + } + }, + "@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": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@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": { + "@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": { + "@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" + } + }, + "@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": { + "@azure/abort-controller": "^2.0.0", + "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 + } + } + }, + "@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==" + }, + "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" + } + }, + "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": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, + "@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": { + "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==" + } + } + }, + "@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": { + "@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==" + } + } + }, + "@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": { + "@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": { + "@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 + } + } + }, + "@azure/logger": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", + "integrity": "sha512-ustrPY8MryhloQj7OWGe+HrYx+aoiOxzbXTtgblbV3xwCqpzUK36phH3XNHQKj3EPonyFUuDTfR3qFhTEAuZEg==", + "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==" + } + } + }, + "@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": { + "@azure/msal-common": "14.10.0" + } + }, + "@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 + }, + "@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": { + "@azure/msal-common": "14.12.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "dependencies": { + "@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 + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "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": { + "@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": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@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-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@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 + }, + "@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": { + "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" + } + } + } + }, + "@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" + } + }, + "@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/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": { + "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" + } + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@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 + }, + "@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/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@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/types": "^7.22.5" + } + }, + "@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/types": "^7.22.5" + } + }, + "@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-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" + } + }, + "@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/types": "^7.22.5" + } + }, + "@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" + } + }, + "@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 + }, + "@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 + }, + "@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 + }, + "@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/template": "^7.27.1", + "@babel/types": "^7.27.1" + } + }, + "@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/types": "^7.27.1" + } + }, + "@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "dev": true + }, + "@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": { + "core-js-pure": "^3.30.2" + } + }, + "@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/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": { + "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" + } + } + } + }, + "@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" + } + }, + "@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 + }, + "@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": { + "@cspotcode/source-map-consumer": "0.8.0" + } + }, + "@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": { + "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": { + "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 + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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 + }, + "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": { + "ms": "^2.1.3" + } + }, + "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" + } + }, + "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 + } + } + }, + "@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": { + "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": { + "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": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "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": { + "ansi-regex": "^6.0.1" + } + }, + "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": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@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": { + "@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" + } + } + } + }, + "@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": { + "@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": { + "@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": { + "@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" + } + } + } + }, + "@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": { + "@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": { + "@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": { + "@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" + } + } + } + }, + "@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": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@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": { + "type-detect": "4.0.8" + } + }, + "@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": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@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": { + "type-detect": "4.0.8" + } + } + } + }, + "@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 + }, + "@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 + }, + "@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "@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 + }, + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "@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": { + "@types/node": "*" + } + }, + "@types/chai": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", + "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", + "dev": true + }, + "@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": "*" + } + }, + "@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": { + "@types/chai": "*" + } + }, + "@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/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": "*" + } + }, + "@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": "*" + } + }, + "@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": "*" + } + }, + "@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": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@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 + }, + "@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": "*" + } + }, + "@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": "*" + } + }, + "@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": { + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + }, + "dependencies": { + "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 + } + } + }, + "@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 + }, + "@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": { + "@types/node": "*" + } + }, + "@types/lodash": { + "version": "4.14.181", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz", + "integrity": "sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==", + "dev": true + }, + "@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": { + "undici-types": "~6.21.0" + } + }, + "@types/semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", + "dev": true + }, + "@types/shimmer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.2.tgz", + "integrity": "sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg==" + }, + "@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": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@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 + }, + "@types/stack-trace": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.29.tgz", + "integrity": "sha512-TgfOX+mGY/NyNxJLIbDWrO9DjGoVSW9+aB8H2yy1fy32jsvxijhmyJI9fDFgvz3YP4lvJaq9DzdR/M1bOgVc9g==", + "dev": true + }, + "@types/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", + "dev": true + }, + "@types/tough-cookie": { + "version": "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 + }, + "@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 + }, + "@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 + }, + "@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 + }, + "@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": { + "@types/node": "*" + } + }, + "@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" + } + } + } + }, + "@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": { + "@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": { + "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" + } + } + } + }, + "@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": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + } + }, + "@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": { + "@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" + } + } + } + }, + "@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 + }, + "@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": { + "@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": { + "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" + } + } + } + }, + "@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": { + "@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": { + "@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + } + } + }, + "@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": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + } + }, + "@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 + }, + "@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": { + "@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" + } + }, + "@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" + } + }, + "@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": { + "@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": { + "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": { + "lru-cache": "^6.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" + } + } + } + }, + "@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": { + "@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" + } + }, + "@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 + }, + "@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 + }, + "@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 + }, + "@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 + }, + "@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 + }, + "@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 + }, + "@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 + }, + "@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 + }, + "@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 + }, + "@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" + } + }, + "@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 + }, + "@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 + }, + "@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 + }, + "@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" + } + }, + "@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 + }, + "@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" + } + }, + "@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" + } + }, + "@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" + } + }, + "@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 + }, + "@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" + } + }, + "@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" + } + }, + "@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" + } + }, + "@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" + } + }, + "@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": { + "envinfo": "^7.7.3" + } + }, + "@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": {} + }, + "@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": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==" + }, + "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": {} + }, + "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 + }, + "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": { + "debug": "4" + }, + "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" + } + } + } + }, + "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": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "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-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": { + "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 + } + } + }, + "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": {} + }, + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + }, + "ansi-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": { + "color-convert": "^1.9.0" + } + }, + "ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "dev": true + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "append-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", + "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", + "dev": true, + "requires": { + "buffer-equal": "^1.0.0" + }, + "dependencies": { + "buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", + "dev": true + } + } + }, + "append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "requires": { + "default-require-extensions": "^3.0.0" + } + }, + "applicationinsights": { + "version": "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" + } + }, + "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": { + "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 + } + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "arg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", + "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "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-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-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": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + } + }, + "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": { + "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" + } + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true + }, + "array-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 + }, + "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": { + "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" + } + }, + "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": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "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": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "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": { + "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" + } + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "ast-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 + }, + "async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", + "dev": true + }, + "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": { + "end-of-stream": "^1.4.4", + "once": "^1.4.0", + "stream-exhaust": "^1.0.2" + } + }, + "async-hook-jl": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", + "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", + "requires": { + "stack-chain": "^1.3.7" + } + }, + "async-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" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" + } + } + }, + "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" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "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": { + "possible-typed-array-names": "^1.0.0" + } + }, + "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 + }, + "axobject-query": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", + "dev": true + }, + "azure-devops-node-api": { + "version": "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": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "core-js": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", + "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==", + "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 + } + } + }, + "bach": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", + "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", + "dev": true, + "requires": { + "async-done": "^2.0.0", + "async-settle": "^2.0.0", + "now-and-later": "^3.0.0" + }, + "dependencies": { + "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": { + "once": "^1.4.0" + } + } + } + }, + "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": { + "bytesish": "^0.4.1", + "caseless": "~0.12.0", + "is-stream": "^2.0.0" + }, + "dependencies": { + "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 + } + } + }, + "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": { + "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" + } + }, + "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.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "dev": true, + "requires": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "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 + } + } + }, + "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": { + "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": { + "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 + } + } + }, + "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" + } + }, + "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" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "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": { + "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 + }, + "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 + } + } + }, + "caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "requires": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + } + }, + "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" + } + }, + "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 + }, + "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 + }, + "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 + }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "^1.0.2" + } + }, + "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" + } + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "dev": true + }, + "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": { + "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": { + "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": { + "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 + }, + "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": { + "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 + } + } + }, + "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": { + "css-select": "^4.1.3", + "css-what": "^5.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0", + "domutils": "^2.7.0" + }, + "dependencies": { + "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": { + "boolbase": "^1.0.0", + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", + "nth-check": "^2.0.0" + } + }, + "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 + }, + "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": { + "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 + } + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "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" + } + } + } + }, + "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 + }, + "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.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "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 + } + } + }, + "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 + }, + "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==" + }, + "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 + }, + "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": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, + "clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", + "dev": true + }, + "clone-deep": { + "version": "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": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, + "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" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" + } + } + }, + "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 + }, + "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 + }, + "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": { + "delayed-stream": "~1.0.0" + } + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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": { + "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 + } + } + }, + "continuation-local-storage": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", + "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==", + "requires": { + "async-listener": "^0.6.0", + "emitter-listener": "^1.1.1" + } + }, + "convert-source-map": { + "version": "1.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 + }, + "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": { + "each-props": "^3.0.0", + "is-plain-object": "^5.0.0" + }, + "dependencies": { + "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 + } + } + }, + "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": { + "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" + } + } + } + }, + "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": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "create-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 + }, + "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": { + "cross-spawn": "^7.0.1" + }, + "dependencies": { + "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" + } + }, + "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", - "bundled": true, + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + } + } + }, + "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": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, - "append-transform": { - "version": "1.0.0", - "bundled": true, + "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" + } + } + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "dev": true + }, + "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 + }, + "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" + } + }, + "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": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "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": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "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": { + "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": { - "default-require-extensions": "^2.0.0" + "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 + } } }, - "archy": { - "version": "1.0.0", - "bundled": 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 - }, - "arrify": { - "version": "1.0.1", - "bundled": true, + } + } + }, + "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": { + "mimic-response": "^1.0.0" + } + }, + "decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "requires": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", "dev": true - }, - "async": { - "version": "1.5.2", - "bundled": true, + } + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "dependencies": { + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "builtin-modules": { - "version": "1.1.1", - "bundled": true, + } + } + }, + "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": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "dependencies": { + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", "dev": true }, - "caching-transform": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "make-dir": "^1.0.0", - "md5-hex": "^2.0.0", - "package-hash": "^2.0.0", - "write-file-atomic": "^2.0.0" - } - }, - "camelcase": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true - }, - "center-align": { - "version": "0.1.3", - "bundled": 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, - "optional": true, "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" } }, - "cliui": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + } + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-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 + }, + "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 + }, + "default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "requires": { + "strip-bom": "^4.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + } + } + }, + "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": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "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 + }, + "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": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", + "dev": true, + "requires": { + "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" + } + }, + "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": { + "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": "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": { + "semver": "^7.5.3" + } + }, + "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": {} + }, + "diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "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" + } + }, + "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": { + "path-type": "^4.0.0" + } + }, + "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" + } + }, + "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": { + "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": { + "make-dir": { "version": "2.1.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, - "optional": true, "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "bundled": true, - "dev": true, - "optional": true - } + "pify": "^4.0.1", + "semver": "^5.6.0" } }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "commondir": { - "version": "1.0.1", - "bundled": true, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": 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": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "duplexer3": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", + "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" + } + }, + "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": { + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0" + }, + "dependencies": { + "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 - }, - "convert-source-map": { - "version": "1.6.0", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "cross-spawn": { - "version": "4.0.2", - "bundled": true, - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "debug": { - "version": "3.1.0", - "bundled": true, + } + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "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": { + "safe-buffer": "^5.0.1" + } + }, + "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 + }, + "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": { + "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" + } + }, + "emitter-listener": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", + "requires": { + "shimmer": "^1.2.0" + } + }, + "emoji-regex": { + "version": "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": { + "once": "^1.4.0" + } + }, + "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": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + } + }, + "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/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 + }, + "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": { + "es-errors": "^1.3.0" + } + }, + "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/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "requires": { + "hasown": "^2.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==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "es6-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 + }, + "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": { - "ms": "2.0.0" + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" } }, - "debug-log": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "decamelize": { - "version": "1.2.0", - "bundled": true, + "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 }, - "default-require-extensions": { - "version": "2.0.0", - "bundled": 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": { - "strip-bom": "^3.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, - "error-ex": { - "version": "1.3.2", - "bundled": 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": { - "is-arrayish": "^0.2.1" + "color-name": "~1.1.4" } }, - "es6-error": { - "version": "4.1.1", - "bundled": true, + "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 }, - "execa": { - "version": "0.7.0", - "bundled": 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": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "5.1.0", - "bundled": true, - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - } + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, - "find-cache-dir": { - "version": "2.0.0", - "bundled": true, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { - "commondir": "^1.0.1", - "make-dir": "^1.0.0", - "pkg-dir": "^3.0.0" + "ms": "2.1.2" } }, - "find-up": { + "doctrine": { "version": "3.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "esutils": "^2.0.2" } }, - "foreground-child": { - "version": "1.5.6", - "bundled": true, + "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": { - "cross-spawn": "^4", - "signal-exit": "^3.0.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" } }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "bundled": true, - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "bundled": true, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, - "glob": { - "version": "7.1.3", - "bundled": 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": { - "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" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" } }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true, - "dev": true - }, - "handlebars": { - "version": "4.0.11", - "bundled": true, + "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": "^1.4.0", - "optimist": "^0.6.1", - "source-map": "^0.4.4", - "uglify-js": "^2.6" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "bundled": true, - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } + "is-glob": "^4.0.3" } }, - "has-flag": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "hosted-git-info": { - "version": "2.7.1", - "bundled": true, - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "bundled": true, - "dev": true - }, - "inflight": { - "version": "1.0.6", - "bundled": true, + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { - "once": "^1.3.0", - "wrappy": "1" + "type-fest": "^0.20.2" } }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "invert-kv": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "bundled": true, - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "bundled": 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 }, - "is-builtin-module": { - "version": "1.0.0", - "bundled": 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": { - "builtin-modules": "^1.0.0" + "argparse": "^2.0.1" } }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "isexe": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "istanbul-lib-coverage": { - "version": "2.0.1", - "bundled": true, - "dev": true - }, - "istanbul-lib-hook": { - "version": "2.0.1", - "bundled": true, + "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": { - "append-transform": "^1.0.0" + "p-locate": "^5.0.0" } }, - "istanbul-lib-report": { - "version": "2.0.2", - "bundled": 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": { - "istanbul-lib-coverage": "^2.0.1", - "make-dir": "^1.3.0", - "supports-color": "^5.4.0" + "brace-expansion": "^1.1.7" } }, - "istanbul-lib-source-maps": { - "version": "2.0.1", - "bundled": true, + "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": { - "debug": "^3.1.0", - "istanbul-lib-coverage": "^2.0.1", - "make-dir": "^1.3.0", - "rimraf": "^2.6.2", - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "bundled": true, - "dev": true - } + "yocto-queue": "^0.1.0" } }, - "istanbul-reports": { - "version": "2.0.1", - "bundled": true, + "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": { - "handlebars": "^4.0.11" + "p-limit": "^3.0.2" } }, - "json-parse-better-errors": { - "version": "1.0.2", - "bundled": 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 }, - "kind-of": { - "version": "3.2.2", - "bundled": 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": { - "is-buffer": "^1.1.5" + "shebang-regex": "^3.0.0" } }, - "lazy-cache": { - "version": "1.0.4", - "bundled": true, - "dev": true, - "optional": true + "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 }, - "lcid": { - "version": "1.0.0", - "bundled": 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": { - "invert-kv": "^1.0.0" + "has-flag": "^4.0.0" } }, - "load-json-file": { - "version": "4.0.0", - "bundled": true, + "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-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": {} + }, + "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": { + "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": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" + "ms": "^2.1.1" } - }, - "locate-path": { - "version": "3.0.0", - "bundled": true, + } + } + }, + "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": { + "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": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "ms": "^2.1.1" } - }, - "lodash.flattendeep": { - "version": "4.4.0", - "bundled": true, - "dev": true - }, - "longest": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "lru-cache": { - "version": "4.1.3", - "bundled": true, + } + } + }, + "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": { + "@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": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "ms": "^2.1.1" } }, - "make-dir": { - "version": "1.3.0", - "bundled": 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": { - "pify": "^3.0.0" + "brace-expansion": "^1.1.7" } }, - "md5-hex": { - "version": "2.0.0", - "bundled": true, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "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": { + "@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": { + "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": { - "md5-o-matic": "^0.1.1" + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" } }, - "md5-o-matic": { - "version": "0.1.1", - "bundled": 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 }, - "mem": { - "version": "1.1.0", - "bundled": 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": { - "mimic-fn": "^1.0.0" + "brace-expansion": "^1.1.7" } + } + } + }, + "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 + }, + "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": { + "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 }, - "merge-source-map": { - "version": "1.1.0", - "bundled": 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": { - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "bundled": true, - "dev": true - } + "brace-expansion": "^1.1.7" } }, - "mimic-fn": { - "version": "1.2.0", - "bundled": true, - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, + "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": { - "brace-expansion": "^1.1.7" + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" } }, - "minimist": { - "version": "0.0.10", - "bundled": true, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, + } + } + }, + "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": {} + }, + "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": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "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 + }, + "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": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "requires": { + "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 + } + } + }, + "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": { + "estraverse": "^5.2.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 + } + } + }, + "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.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": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "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": { + "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": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - } + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, - "ms": { - "version": "2.0.0", - "bundled": true, + "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 }, - "normalize-package-data": { - "version": "2.4.0", - "bundled": true, + "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 + }, + "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": { - "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" + "shebang-regex": "^3.0.0" } }, - "npm-run-path": { - "version": "2.0.2", - "bundled": true, + "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 + } + } + }, + "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": { + "homedir-polyfill": "^1.0.1" + } + }, + "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": { + "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" + } + } + } + }, + "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 + }, + "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": { + "@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": { + "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": { - "path-key": "^2.0.0" + "is-glob": "^4.0.1" } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": 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=", + "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 + }, + "fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true + }, + "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 + }, + "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": { + "flat-cache": "^3.0.4" + } + }, + "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 + }, + "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": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + } + }, + "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 + }, + "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": { + "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" + } + }, + "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": { + "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": { + "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 - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optimist": { - "version": "0.6.1", - "bundled": true, + } + } + }, + "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 + }, + "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": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "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": { + "is-callable": "^1.2.7" + } + }, + "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": { + "for-in": "^1.0.1" + } + }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, - "os-homedir": { - "version": "1.0.2", - "bundled": 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 }, - "os-locale": { - "version": "2.1.0", - "bundled": 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": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" + "shebang-regex": "^3.0.0" } }, - "p-finally": { - "version": "1.0.0", - "bundled": true, + "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 + } + } + }, + "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": { + "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": { + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } }, - "p-limit": { + "universalify": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + } + } + }, + "fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + } + }, + "fs-walk": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/fs-walk/-/fs-walk-0.0.1.tgz", + "integrity": "sha1-9/yRw64e6tB8mYvF0N1B8tvr0zU=", + "dev": true, + "requires": { + "async": "*" + } + }, + "fs.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": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + } + }, + "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": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "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" + }, + "dependencies": { + "pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "dev": true, "requires": { - "p-try": "^2.0.0" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } - }, - "p-locate": { - "version": "3.0.0", - "bundled": true, - "dev": true, + } + } + }, + "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" + } + }, + "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": { + "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": { + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "requires": { - "p-limit": "^2.0.0" + "brace-expansion": "^1.1.7" } - }, - "p-try": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "package-hash": { - "version": "2.0.0", - "bundled": true, + } + } + }, + "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": { + "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": { - "graceful-fs": "^4.1.11", - "lodash.flattendeep": "^4.4.0", - "md5-hex": "^2.0.0", - "release-zalgo": "^1.0.0" + "is-extglob": "^2.1.0" } - }, - "parse-json": { - "version": "4.0.0", - "bundled": true, + } + } + }, + "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": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + } + }, + "glob-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": { + "async-done": "^2.0.0", + "chokidar": "^3.5.3" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "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": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "isexe": "^2.0.0" } - }, - "path-exists": { + } + } + }, + "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 + }, + "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": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "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" + } + }, + "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": { + "sparkles": "^2.1.0" + } + }, + "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": { + "@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": { + "get-stream": { "version": "3.0.0", - "bundled": true, - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "path-key": { - "version": "2.0.1", - "bundled": true, + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", "dev": true }, - "path-type": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, "pify": { "version": "3.0.0", - "bundled": true, - "dev": true - }, - "pkg-dir": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, - "pseudomap": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "read-pkg": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "4.0.0", - "bundled": true, - "dev": true, - "requires": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" - } - }, - "release-zalgo": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "es6-error": "^4.0.1" - } - }, - "repeat-string": { - "version": "1.6.1", - "bundled": true, - "dev": true - }, - "require-directory": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "bundled": true, - "dev": true - }, - "right-align": { - "version": "0.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.1" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": 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==" + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "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": { + "glob-watcher": "^6.0.0", + "gulp-cli": "^3.0.0", + "undertaker": "^2.0.0", + "vinyl-fs": "^4.0.0" + }, + "dependencies": { + "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 }, - "source-map": { - "version": "0.5.7", - "bundled": 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, - "optional": true + "requires": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + } }, - "spawn-wrap": { - "version": "1.4.2", - "bundled": true, + "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": { - "foreground-child": "^1.5.6", - "mkdirp": "^0.5.0", - "os-homedir": "^1.0.1", - "rimraf": "^2.6.2", - "signal-exit": "^3.0.2", - "which": "^1.3.0" + "is-glob": "^4.0.3" } }, - "spdx-correct": { - "version": "3.0.0", - "bundled": true, + "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": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.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" } }, - "spdx-exceptions": { - "version": "2.1.0", - "bundled": true, + "lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", "dev": true }, - "spdx-expression-parse": { + "now-and-later": { "version": "3.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", "dev": true, "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "once": "^1.4.0" } }, - "spdx-license-ids": { - "version": "3.0.0", - "bundled": true, + "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 }, - "string-width": { - "version": "2.1.1", - "bundled": 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": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "value-or-function": "^4.0.0" } }, - "strip-ansi": { - "version": "4.0.0", - "bundled": true, + "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": { - "ansi-regex": "^3.0.0" + "streamx": "^2.12.5" } }, - "strip-bom": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "strip-eof": { - "version": "1.0.0", - "bundled": true, + "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 }, - "supports-color": { - "version": "5.4.0", - "bundled": 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": { - "has-flag": "^3.0.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" } }, - "test-exclude": { - "version": "5.0.0", - "bundled": true, + "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": { - "arrify": "^1.0.1", - "minimatch": "^3.0.4", - "read-pkg-up": "^4.0.0", - "require-main-filename": "^1.0.1" - } - }, - "uglify-js": { - "version": "2.8.29", - "bundled": true, + "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, - "optional": true, "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - }, - "dependencies": { - "yargs": { - "version": "3.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.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" } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "bundled": true, + } + } + }, + "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": { + "@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": { + "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, - "optional": true - }, - "uuid": { - "version": "3.3.2", - "bundled": true, - "dev": true + "requires": { + "color-convert": "^2.0.1" + } }, - "validate-npm-package-license": { - "version": "3.0.3", - "bundled": true, + "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": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, - "which": { - "version": "1.3.1", - "bundled": 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": { - "isexe": "^2.0.0" + "color-name": "~1.1.4" } }, - "which-module": { - "version": "2.0.0", - "bundled": true, + "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 }, - "window-size": { - "version": "0.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "wordwrap": { - "version": "0.0.3", - "bundled": 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 }, - "wrap-ansi": { - "version": "2.1.0", - "bundled": 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": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } + "has-flag": "^4.0.0" } }, - "wrappy": { - "version": "1.0.2", - "bundled": true, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, - "write-file-atomic": { - "version": "2.3.0", - "bundled": true, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" + "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" } - }, - "y18n": { - "version": "3.2.1", - "bundled": true, + } + } + }, + "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": { + "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": { + "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 }, - "yallist": { - "version": "2.1.2", - "bundled": 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 }, - "yargs": { - "version": "11.1.0", - "bundled": true, + "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": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + } + } + }, + "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": { + "glogg": "^2.2.0" + } + }, + "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": { + "duplexer": "^0.1.2" + } + }, + "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": { + "function-bind": "^1.1.1" + } + }, + "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": { + "es-define-property": "^1.0.0" + } + }, + "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 + }, + "has-symbol-support-x": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", + "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", + "dev": true + }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "has-to-string-tag-x": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", + "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "dev": true, + "requires": { + "has-symbol-support-x": "^1.4.1" + } + }, + "has-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" + } + }, + "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": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hasha": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.1.0.tgz", + "integrity": "sha512-OFPDWmzPN1l7atOV1TgBVmNtBxaIysToK6Ve9DK+vT6pYuklw/nPNT+HJbZi0KDcI6vWB+9tgvZ5YD7fA3CXcA==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + } + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "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" + } + }, + "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 + }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "dev": true + }, + "http-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" + }, + "dependencies": { + "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": { - "cliui": "^4.0.0", - "decamelize": "^1.1.1", - "find-up": "^2.1.0", - "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^9.0.2" - }, - "dependencies": { - "cliui": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "bundled": true, - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "bundled": true, - "dev": true - } + "ms": "2.1.2" } - }, - "yargs-parser": { - "version": "9.0.2", - "bundled": true, - "dev": true, + } + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "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": { - "camelcase": "^4.1.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "bundled": true, - "dev": true - } + "ms": "2.1.2" } } } }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + "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 }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "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": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, + "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": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" + "parent-module": "^1.0.0", + "resolve-from": "^4.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=", - "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=", - "requires": { - "is-buffer": "^1.1.5" - } + "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 } } }, - "object-inspect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", - "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", + "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": { + "acorn": "^8.8.2", + "acorn-import-assertions": "^1.9.0", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "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": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, - "object-is": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", - "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", + "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 }, - "object-keys": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", - "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.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": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true + }, + "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": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + } + }, + "inversify": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-6.0.2.tgz", + "integrity": "sha512-i9m8j/7YIv4mDuYXUAcrpKPSaju/CIly9AHK5jvCBeoiM/2KEsuCQTTP+rzSWWpLYWRukdXFSl6ZTk2/uumbiA==" + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-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": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "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": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + } + }, + "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": { + "has-bigints": "^1.0.1" + } + }, + "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-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": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.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-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 }, - "object-visit": { + "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": { + "hasown": "^2.0.2" + } + }, + "is-data-view": { "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/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "requires": { + "is-typed-array": "^1.1.13" + } + }, + "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": { + "has-tostringtag": "^1.0.0" + } + }, + "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 + }, + "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": "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 + }, + "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": { + "has-tostringtag": "^1.0.0" + } + }, + "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": { + "is-extglob": "^2.1.1" + } + }, + "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": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "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 + }, + "is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.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 + }, + "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": { + "has-tostringtag": "^1.0.0" + } + }, + "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": { - "isobject": "^3.0.0" + "isobject": "^3.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==", + "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": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" } }, - "object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "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": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" + "is-unc-path": "^1.0.0" } }, - "object.entries": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz", - "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=", + "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 + }, + "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": { - "define-properties": "^1.1.2", - "es-abstract": "^1.6.1", - "function-bind": "^1.1.0", - "has": "^1.0.1" + "call-bind": "^1.0.7" } }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "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 + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" + "has-tostringtag": "^1.0.0" } }, - "object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "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": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" + "has-symbols": "^1.0.2" } }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "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": { - "for-own": "^0.1.4", - "is-extendable": "^0.1.1" - }, - "dependencies": { - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - } + "which-typed-array": "^1.1.16" } }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "^3.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 }, - "object.reduce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", - "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", + "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": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" + "unc-path-regex": "^0.1.2" } }, - "object.values": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.0.4.tgz", - "integrity": "sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo=", + "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 + }, + "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-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": { - "define-properties": "^1.1.2", - "es-abstract": "^1.6.1", - "function-bind": "^1.1.0", - "has": "^1.0.1" + "call-bind": "^1.0.2" } }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "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": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, "requires": { - "ee-first": "1.1.1" + "is-docker": "^2.0.0" } }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "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 + }, + "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": { - "mimic-fn": "^1.0.0" + "append-transform": "^2.0.0" } }, - "opener": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz", - "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==", - "dev": true - }, - "opn": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", - "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "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": { - "is-wsl": "^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 + } } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "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": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" + "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": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "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" + } + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "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 }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true } } }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "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": { - "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" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "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" + } + } } }, - "options": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", - "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=" - }, - "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=", + "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": { - "readable-stream": "^2.0.1" + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "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" + } + } } }, - "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==", + "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": { - "url-parse": "^1.4.3" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" } }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", - "dev": true - }, - "os-locale": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.0.1.tgz", - "integrity": "sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw==", + "isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", "dev": true, "requires": { - "execa": "^0.10.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" } }, - "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=" - }, - "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 + "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": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } }, - "p-event": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-1.3.0.tgz", - "integrity": "sha1-jmtPT2XHK8W2/ii3XtqHT5akoIU=", + "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": { - "p-timeout": "^1.1.1" + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, "dependencies": { - "p-timeout": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", - "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", + "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": { - "p-finally": "^1.0.0" + "has-flag": "^4.0.0" } } } }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "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=", + "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 }, - "p-limit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", - "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", + "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": { - "p-try": "^2.0.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, "dependencies": { - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", + "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 } } }, - "p-locate": { + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-buffer": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.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 }, - "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "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 + }, + "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 }, - "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": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "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 }, - "pako": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", - "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "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=", - "dev": true, - "requires": { - "cyclist": "~0.2.2", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" + "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==" + }, + "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": { + "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": { - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "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": { - "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" + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.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==", + "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": { - "safe-buffer": "~5.1.0" + "jwa": "^1.4.2", + "safe-buffer": "^5.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=", + "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": { - "no-case": "^2.2.0" + "array-includes": "^3.1.3", + "object.assign": "^4.1.2" } }, - "parse-asn1": { - "version": "5.1.1", - "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "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": { - "asn1.js": "^4.0.0", - "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" } }, - "parse-entities": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.0.tgz", - "integrity": "sha512-XXtDdOPLSB0sHecbEapQi6/58U/ODj/KWfIXmmMCJF/eRn8laX6LZbOyioMoETOOJoWRW8/qTSl5VQkUIfKM5g==", - "dev": true, - "requires": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } + "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 }, - "parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "dev": true, "requires": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "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": { - "glob-base": "^0.3.0", - "is-dotfile": "^1.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.0" - }, - "dependencies": { - "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true - }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "dev": true, - "requires": { - "is-extglob": "^1.0.0" - } - } + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" } }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "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": { - "error-ex": "^1.2.0" + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.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 - }, - "parse5": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", - "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", + "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": { - "@types/node": "*" + "json-buffer": "3.0.0" } }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "path-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", - "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", + "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 }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "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 }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "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": { - "pinkie-promise": "^2.0.0" + "language-subtag-registry": "~0.3.2" } }, - "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.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "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 }, - "path-posix": { + "lazystream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", - "integrity": "sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8=" + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "dev": true, + "requires": { + "readable-stream": "^2.0.5" + } }, - "path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", "dev": true, "requires": { - "path-root-regex": "^0.1.0" + "flush-write-stream": "^1.0.2" } }, - "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=", + "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 }, - "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 + "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" + } }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "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": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" + "immediate": "~3.0.5" + } + }, + "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": { + "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": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "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 } } }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "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": { + "uc.micro": "^1.0.1" + } + }, + "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 }, - "pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "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": { - "through": "~2.3" + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" } }, - "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "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": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "p-locate": "^4.1.0" } }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, - "pidusage": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-1.2.0.tgz", - "integrity": "sha512-OGo+iSOk44HRJ8q15AyG570UYxcm5u+R99DI8Khu8P3tKGkVu5EZX4ywHglWSTMNNXQ274oeGpYrvFEhDIFGPg==" + "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 }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "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 }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "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 }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } + "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 }, - "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" - }, - "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" - } - } - } + "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 }, - "please-upgrade-node": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz", - "integrity": "sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ==", - "dev": true, - "requires": { - "semver-compare": "^1.0.0" - } + "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 }, - "plugin-error": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", - "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", - "dev": true, - "requires": { - "ansi-cyan": "^0.1.1", - "ansi-red": "^0.1.1", - "arr-diff": "^1.0.1", - "arr-union": "^2.0.1", - "extend-shallow": "^1.1.2" - }, - "dependencies": { - "arr-diff": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", - "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1", - "array-slice": "^0.2.3" - } - }, - "arr-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", - "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", - "dev": true - }, - "array-slice": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", - "dev": true - }, - "extend-shallow": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", - "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", - "dev": true, - "requires": { - "kind-of": "^1.1.0" - } - }, - "kind-of": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", - "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", - "dev": true - } - } + "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 }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "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 }, - "polygon-offset": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/polygon-offset/-/polygon-offset-0.3.1.tgz", - "integrity": "sha1-aaZWXwsn+na1Jw1cB5sLosjwu6M=", - "dev": true, - "requires": { - "martinez-polygon-clipping": "^0.1.5" - } + "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 }, - "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=" + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true }, - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "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": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "dependencies": { "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==", + "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": "^1.9.0" + "color-convert": "^2.0.1" } }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "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": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "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 }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "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": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "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" } } } }, - "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==", + "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": { - "postcss": "^6.0.1" + "js-tokens": "^3.0.0 || ^4.0.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=", + "loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", "dev": true, "requires": { - "css-selector-tokenizer": "^0.7.0", - "postcss": "^6.0.1" + "get-func-name": "^2.0.0" } }, - "postcss-modules-scope": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", - "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", - "dev": true, + "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 + }, + "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": { - "css-selector-tokenizer": "^0.7.0", - "postcss": "^6.0.1" + "yallist": "^4.0.0" } }, - "postcss-modules-values": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", - "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", + "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": { - "icss-replace-symbols": "^1.1.0", - "postcss": "^6.0.1" + "semver": "^6.0.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 + } } }, - "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==", + "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 }, - "postinstall-build": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.1.tgz", - "integrity": "sha1-uRepB5smF42aJK9aXNjLSpkdEbk=", + "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 }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "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": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "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 + } + } + }, + "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 }, - "prepend-http": { + "merge-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, - "pretty-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", - "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "renderkid": "^2.0.1", - "utila": "~0.4" + "braces": "^3.0.3", + "picomatch": "^2.3.1" } }, - "pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", - "dev": true - }, - "prismjs": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.15.0.tgz", - "integrity": "sha512-Lf2JrFYx8FanHrjoV5oL8YHCclLQgbJcVZR+gikGGMqz6ub5QVWDTM6YIwm3BuPxM/LOV+rKns3LssXNLIf+DA==", + "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": { - "clipboard": "^2.0.0" + "bn.js": "^4.0.0", + "brorand": "^1.0.1" } }, - "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=", + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "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=" + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, - "promise": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-8.0.1.tgz", - "integrity": "sha1-5F1osAoXZHttpxG/he1u1HII9FA=", - "dev": true, + "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": { - "asap": "~2.0.3" + "mime-db": "1.52.0" } }, - "promise-inflight": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mimic-response": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true }, - "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==", + "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": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "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" + } + } } }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, - "proxy-addr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "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": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.8.0" + "minimist": "^1.2.5" + } + }, + "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 + }, + "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": { + "balanced-match": "^1.0.0" + } + }, + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "requires": { + "readdirp": "^4.0.1" + } + }, + "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 + }, + "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 + }, + "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" + } + }, + "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": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + } + }, + "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", + "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": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.2" + } + }, + "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 + }, + "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 + }, + "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": { + "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 + }, + "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": { + "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 + }, + "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": { + "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 + } } }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "psl": { - "version": "1.1.29", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" - }, - "public-encrypt": { - "version": "4.0.2", - "resolved": "http://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", - "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==", + "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": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1" + "debug": "^2.2.0", + "md5": "^2.1.0", + "mkdirp": "~0.5.1", + "strip-ansi": "^6.0.1", + "xml": "^1.0.0" } }, - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "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": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "debug": "^4.1.1", + "lodash": "^4.17.15" }, "dependencies": { - "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==", + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { - "once": "^1.4.0" + "ms": "2.1.2" } } } }, - "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" - } + "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==" }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + "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 }, - "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=", + "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 }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "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==" + "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==" }, - "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==", + "nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true + }, + "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": { - "decode-uri-component": "^0.2.0", - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } + "optional": true }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "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 }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "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 }, - "querystringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz", - "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==" - }, - "queue": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/queue/-/queue-3.1.0.tgz", - "integrity": "sha1-bEnQHwCeIlZ4h4nyv/rGuLmZBYU=", - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "raf": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", - "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", - "dev": true, - "requires": { - "performance-now": "^2.1.0" - } - }, - "railroad-diagrams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", - "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=", + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, - "randexp": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", - "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", "dev": true, "requires": { - "discontinuous-range": "1.0.0", - "ret": "~0.1.10" + "@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" } }, - "randomatic": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz", - "integrity": "sha512-VdxFOIEY3mNO5PtSRkkle/hPJDHvQhK21oa73K4yAc9qmp6N429gAyF1gZMOTMeS0/AYzaV/2Trcef+NaIonSA==", + "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-number": "^4.0.0", - "kind-of": "^6.0.0", - "math-random": "^1.0.1" - }, - "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 - } + "semver": "^7.3.5" } }, - "randombytes": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", - "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "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, - "requires": { - "safe-buffer": "^5.1.0" - } + "optional": true }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "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": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" + "fs-walk": "0.0.1" } }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", - "dev": true - }, - "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "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": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", - "unpipe": "1.0.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": { - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "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 } } }, - "raw-loader": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", - "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=", - "dev": true - }, - "react": { - "version": "16.5.2", - "resolved": "https://registry.npmjs.org/react/-/react-16.5.2.tgz", - "integrity": "sha512-FDCSVd3DjVTmbEAjUNX6FgfAmQ+ypJfHUsqUJOYNCBUp1h8lqmtC+0mXJ+JjsWx4KAVTkk1vKd1hLQPvEviSuw==", + "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": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "schedule": "^0.5.0" + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" } }, - "react-annotation": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/react-annotation/-/react-annotation-1.3.1.tgz", - "integrity": "sha512-jwbl7v5fMvkQrqdFWIIjuziqUzvEwyzhhSJ61bFjxPDEIizRuWf0ym6o6dV034toePs429sG6btGaLWGMC9zEw==", + "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": { - "prop-types": "15.6.0", - "viz-annotation": "0.0.1-3" + "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": { - "prop-types": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", + "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": { - "fbjs": "^0.8.16", - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" + "es6-object-assign": "^1.1.0", + "is-nan": "^1.2.1", + "object-is": "^1.0.1", + "util": "^0.12.0" } - } - } - }, - "react-base16-styling": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.5.3.tgz", - "integrity": "sha1-OFjyTpxN2MvT9wLz901YHKKRcmk=", - "dev": true, - "requires": { - "base16": "^1.0.0", - "lodash.curry": "^4.0.1", - "lodash.flow": "^3.3.0", - "pure-color": "^1.2.0" - } - }, - "react-color": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.14.1.tgz", - "integrity": "sha512-ssv2ArSZdhTbIs29hyfw8JW+s3G4BCx/ILkwCajWZzrcx/2ZQfRpsaLVt38LAPbxe50LLszlmGtRerA14JzzRw==", - "dev": true, - "requires": { - "lodash": "^4.0.1", - "material-colors": "^1.2.1", - "prop-types": "^15.5.10", - "reactcss": "^1.2.0", - "tinycolor2": "^1.4.1" - } - }, - "react-dev-utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-5.0.2.tgz", - "integrity": "sha512-d2FbKvYe4XAQx5gjHBoWG+ADqC3fGZzjb7i9vxd/Y5xfLkBGtQyX7aOb8lBRQPYUhjngiD3d49LevjY1stUR0Q==", - "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": { - "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=", + }, + "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": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "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": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } }, - "opn": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.2.0.tgz", - "integrity": "sha512-Jd/GpzPyHF4P2/aNOVmS3lfMSWV9J7cOhCG1s08XCEAsPkB7lp6ddiU0J7XzyQRDUh8BqJ7PchfINjR8jyofRQ==", + "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, "requires": { - "is-wsl": "^1.1.0" + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" } - } - } - }, - "react-dom": { - "version": "16.5.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.5.2.tgz", - "integrity": "sha512-RC8LDw8feuZOHVgzEf7f+cxBr/DnKdqp56VU0lAs1f4UfKc4cU8wU4fTq/mgnvynLQo8OtlPC19NUFh/zjZPuA==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "schedule": "^0.5.0" - } - }, - "react-error-overlay": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-4.0.1.tgz", - "integrity": "sha512-xXUbDAZkU08aAkjtUvldqbvI04ogv+a1XdHxvYuHPYKIVk/42BIOD0zSKTHAWV4+gDy3yGm283z2072rA2gdtw==", - "dev": true - }, - "react-hot-loader": { - "version": "4.3.11", - "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.3.11.tgz", - "integrity": "sha512-T0G5jURyTsFLoiW6MTr5Q35UHC/B2pmYJ7+VBjk8yMDCEABRmCGy4g6QwxoB4pWg4/xYvVTa/Pbqnsgx/+NLuA==", - "dev": true, - "requires": { - "fast-levenshtein": "^2.0.6", - "global": "^4.3.0", - "hoist-non-react-statics": "^2.5.0", - "prop-types": "^15.6.1", - "react-lifecycles-compat": "^3.0.4", - "shallowequal": "^1.0.2" - } - }, - "react-inlinesvg": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/react-inlinesvg/-/react-inlinesvg-0.8.3.tgz", - "integrity": "sha512-CQnNFbZtgPC8FyqHJLnurPkljX5z2cKhUUhBqJ1I6BugQMPEwBDxKgTQNAWzhdbYTtXF7sHTzTD79XW5XS2sLw==", - "requires": { - "httpplease": "^0.16.4", - "once": "^1.4.0" - } - }, - "react-is": { - "version": "16.5.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.5.2.tgz", - "integrity": "sha512-hSl7E6l25GTjNEZATqZIuWOgSnpXb3kD0DVCujmg46K5zLxsbiKaaT6VO9slkSBDPZfYs30lwfJwbOFOnoEnKQ==", - "dev": true - }, - "react-json-tree": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.11.0.tgz", - "integrity": "sha1-9bF+gzKanHauOL5cBP2jp/1oSjU=", - "dev": true, - "requires": { - "babel-runtime": "^6.6.1", - "prop-types": "^15.5.8", - "react-base16-styling": "^0.5.1" - } - }, - "react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", - "dev": true - }, - "react-markdown": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-3.6.0.tgz", - "integrity": "sha512-TV0wQDHHPCEeKJHWXFfEAKJ8uSEsJ9LgrMERkXx05WV/3q6Ig+59KDNaTmjcoqlCpE/sH5PqqLMh4t0QWKrJ8Q==", - "dev": true, - "requires": { - "mdast-add-list-metadata": "1.0.1", - "prop-types": "^15.6.1", - "remark-parse": "^5.0.0", - "unified": "^6.1.5", - "unist-util-visit": "^1.3.0", - "xtend": "^4.0.1" + }, + "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": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, + "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 + }, + "util": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", + "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", + "dev": true, + "requires": { + "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" + } + } } }, - "react-table": { - "version": "6.8.6", - "resolved": "https://registry.npmjs.org/react-table/-/react-table-6.8.6.tgz", - "integrity": "sha1-oK2LSDkxkFLVvvwBJgP7Fh5S7eM=", + "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": { - "classnames": "^2.2.5" + "process-on-spawn": "^1.0.0" } }, - "react-table-hoc-fixed-columns": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-table-hoc-fixed-columns/-/react-table-hoc-fixed-columns-1.0.1.tgz", - "integrity": "sha512-0qEHYHA00kBTwUtQnbrJVPh1Ghgx6C6zwq+1cwY4jvjPu0jhYPoAECx0LosphDioA8aaRgkEwDTHDLYcgN9xFQ==", - "dev": true, - "requires": { - "classnames": "^2.2.6", - "emotion": "^9.2.3", - "uniqid": "^5.0.3" - } + "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 }, - "react-test-renderer": { - "version": "16.5.2", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.5.2.tgz", - "integrity": "sha512-AGbJYbCVx1J6jdUgI4s0hNp+9LxlgzKvXl0ROA3DHTrtjAr00Po1RhDZ/eAq2VC/ww8AHgpDXULh5V2rhEqqJg==", - "dev": true, - "requires": { - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "react-is": "^16.5.2", - "schedule": "^0.5.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==" }, - "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" - } + "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 }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "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": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" } }, - "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=", + "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": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "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" + "once": "^1.3.2" } }, - "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "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": { - "graceful-fs": "^4.1.2", - "minimatch": "^3.0.2", - "readable-stream": "^2.0.2", - "set-immediate-shim": "^1.0.1" + "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 + } } }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "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": { - "resolve": "^1.1.6" + "boolbase": "^1.0.0" } }, - "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" + "nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "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", + "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": { - "minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", + "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": { - "brace-expansion": "^1.0.0" + "aggregate-error": "^3.0.0" } } } }, - "reflect-metadata": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz", - "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==" + "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 }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "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 }, - "regenerate-unicode-properties": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz", - "integrity": "sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw==", + "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": { - "regenerate": "^1.4.0" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" } }, - "regenerator-runtime": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", - "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", + "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 }, - "regenerator-transform": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.13.3.tgz", - "integrity": "sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA==", + "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": { - "private": "^0.1.6" + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" } }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "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": { - "is-equal-shallow": "^0.1.3" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" } }, - "regexpu-core": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.2.0.tgz", - "integrity": "sha512-Z835VSnJJ46CNBttalHD/dB+Sj2ezmY6Xp38npwU87peK6mqOzOpV8eYktdkLTEkzzD+JsTcxd84ozd8I14+rw==", + "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": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^7.0.0", - "regjsgen": "^0.4.0", - "regjsparser": "^0.3.0", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.0.2" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" } }, - "regjsgen": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.4.0.tgz", - "integrity": "sha512-X51Lte1gCYUdlwhF28+2YMO0U6WeN0GLpgpA7LK7mbdDnkQYiwvEpmpe0F/cv5L14EbxgrdayAG3JETBv0dbXA==", - "dev": true - }, - "regjsparser": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.3.0.tgz", - "integrity": "sha512-zza72oZBBHzt64G7DxdqrOo/30bhHkwMUoT0WqfGu98XLd7N+1tsy5MJ96Bk4MD0y74n629RhmrGW6XlnLLwCA==", + "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": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - } + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" } }, - "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=", + "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": { - "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" - } - } + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" } }, - "remap-istanbul": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/remap-istanbul/-/remap-istanbul-0.10.1.tgz", - "integrity": "sha512-gsNQXs5kJLhErICSyYhzVZ++C8LBW8dgwr874Y2QvzAUS75zBlD/juZgXs39nbYJ09fZDlX2AVLVJAY2jbFJoQ==", + "object.hasown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz", + "integrity": "sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg==", "dev": true, "requires": { - "amdefine": "^1.0.0", - "istanbul": "0.4.5", - "minimatch": "^3.0.3", - "plugin-error": "^0.1.2", - "source-map": "^0.6.1", - "through2": "2.0.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 - }, - "through2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz", - "integrity": "sha1-OE51MU1J8y3hLuu4E2uOtrXVnak=", - "dev": true, - "requires": { - "readable-stream": "~2.0.0", - "xtend": "~4.0.0" - } - } - } - }, - "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" + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" } }, - "remove-bom-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", - "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", + "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": { - "is-buffer": "^1.1.5", - "is-utf8": "^0.2.1" + "isobject": "^3.0.1" } }, - "remove-bom-stream": { + "object.values": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", - "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, "requires": { - "remove-bom-buffer": "^3.0.0", - "safe-buffer": "^5.1.0", - "through2": "^2.0.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.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=", - "dev": true - }, - "renderkid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.1.tgz", - "integrity": "sha1-iYyr/Ivt5Le5ETWj/9Mj5YwNsxk=", - "dev": true, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { - "css-select": "^1.1.0", - "dom-converter": "~0.1", - "htmlparser2": "~3.3.0", - "strip-ansi": "^3.0.0", - "utila": "~0.3" - }, - "dependencies": { - "domhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.1.0.tgz", - "integrity": "sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ=", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.1.6.tgz", - "integrity": "sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU=", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "htmlparser2": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", - "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", - "dev": true, - "requires": { - "domelementtype": "1", - "domhandler": "2.1", - "domutils": "1.1", - "readable-stream": "1.0" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "utila": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", - "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", - "dev": true - } + "wrappy": "1" } }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, - "replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", - "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=", + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "requires": { - "homedir-polyfill": "^1.0.1", - "is-absolute": "^1.0.0", - "remove-trailing-separator": "^1.1.0" + "mimic-fn": "^2.1.0" } }, - "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "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": { - "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", - "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", - "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "tough-cookie": "~2.3.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" } }, - "request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", - "requires": { - "throttleit": "^1.0.0" - } + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true }, - "request-promise-core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", - "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "requires": { - "lodash": "^4.13.1" + "@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" } }, - "request-promise-native": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", - "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", + "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": { - "request-promise-core": "1.1.1", - "stealthy-require": "^1.1.0", - "tough-cookie": ">=2.3.3" + "readable-stream": "^2.0.1" } }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "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 }, - "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=", + "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 }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" - }, - "resolve": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", - "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "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": { - "path-parse": "^1.0.5" + "p-timeout": "^2.0.1" } }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "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": { - "resolve-from": "^3.0.0" + "p-try": "^2.0.0" } }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "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": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" + "p-limit": "^2.2.0" } }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "resolve-options": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", - "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", + "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": { - "value-or-function": "^3.0.0" + "aggregate-error": "^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=" - }, - "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "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": { - "lowercase-keys": "^1.0.0" + "p-finally": "^1.0.0" } }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", "dev": true, "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" } }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + "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 }, - "retyped-diff-match-patch-tsd-ambient": { - "version": "1.0.0-1", - "resolved": "https://registry.npmjs.org/retyped-diff-match-patch-tsd-ambient/-/retyped-diff-match-patch-tsd-ambient-1.0.0-1.tgz", - "integrity": "sha1-Jkgr9JFcftn4MAu1y+xI/U/1vGI=", + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "dev": true }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "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, - "optional": true, "requires": { - "align-text": "^0.1.1" + "callsites": "^3.0.0" } }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "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": { - "glob": "^7.0.5" + "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 + } } }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "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": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" } }, - "roughjs-es5": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/roughjs-es5/-/roughjs-es5-0.1.0.tgz", - "integrity": "sha512-NMjzoBgSYk8qEYLSxzxytS20sfdQV7zg119FZjFDjIDwaqodFcf7QwzKbqM64VeAYF61qogaPLk3cs8Gb+TqZA==", + "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 + }, + "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": { - "babel-runtime": "^6.26.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 + } } }, - "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=", + "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": { - "lodash.flattendeep": "^4.4.0", - "nearley": "^2.7.10" + "parse5": "^6.0.1" + }, + "dependencies": { + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + } } }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "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": "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": { - "is-promise": "^2.1.0" + "path-root-regex": "^0.1.0" } }, - "run-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", - "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==", + "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 }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "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": { - "aproba": "^1.1.1" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "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 + } } }, - "rx-lite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "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 }, - "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=", + "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 + }, + "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": { - "rx-lite": "*" + "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": { + "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 + } } }, - "rxjs": { - "version": "5.5.9", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.9.tgz", - "integrity": "sha512-DHG9AHmCmgaFWgjBcXp6NxFDmh3MvIA62GqTWmLnTzr/3oZ6h5hLD8NA+9j+GF0jEwklNIpI4KuuyLG8UWMEvQ==", - "requires": { - "symbol-observable": "1.0.1" - } + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "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==" + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "~0.1.10" - } + "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 }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + "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 }, - "saxes": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.3.tgz", - "integrity": "sha512-Nc5DXc5A+m3rUDtkS+vHlBWKT7mCKjJPyia7f8YMW773hsXVv2wEHQZGE0zs4+5PLwz9U5Sbl/94Cnd9vHV7Bg==", + "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": { - "xmlchars": "^1.3.1" + "pinkie": "^2.0.0" } }, - "schedule": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/schedule/-/schedule-0.5.0.tgz", - "integrity": "sha512-HUcJicG5Ou8xfR//c2rPT0lPIRR09vVvN81T9fqfVgBmhERUbDEQoYKjpBxbueJnCPpSu2ujXzOnRQt6x9o/jw==", + "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": { - "object-assign": "^4.1.1" + "find-up": "^4.0.0" } }, - "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==", + "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.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "dependencies": { - "ajv": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.4.tgz", - "integrity": "sha512-4Wyjt8+t6YszqaXnLDfMmG/8AlO5Zbcsy3ATHncCzjW/NoPzAId8AK6749Ybjmdt+kUY1gP60fCu46oDxPv/mg==", - "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" - } - }, - "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=", - "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 - } + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" } }, - "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=", + "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 + }, + "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 + }, + "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": { - "commander": "~2.8.1" + "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": { - "commander": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", - "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "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": { - "graceful-readlink": ">= 1.0.0" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } } } }, - "select": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", - "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=", - "dev": true, - "optional": 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 }, - "semiotic": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/semiotic/-/semiotic-1.15.1.tgz", - "integrity": "sha512-29PHBRq/Y/0Zhw2ancuBt19FRPzuCUXMhcy1WHFSxCvSv5XrknQnV0Jiy7vdDoCB4WcGk+zYZPVfqHzZwUNbgg==", - "dev": true, - "requires": { - "@mapbox/polylabel": "1", - "d3-array": "^1.2.0", - "d3-bboxCollide": "^1.0.3", - "d3-brush": "^1.0.4", - "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.0.4", - "d3-voronoi": "^1.0.2", - "json2csv": "3.11.5", - "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": "1.3.1", - "roughjs-es5": "0.1.0", - "semiotic-mark": "0.3.0", - "svg-path-bounding-box": "1.0.4" - }, - "dependencies": { - "prop-types": { - "version": "15.6.0", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz", - "integrity": "sha1-zq8IMCL8RrSjX2nhPvda7Q1jmFY=", - "dev": true, - "requires": { - "fbjs": "^0.8.16", - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - } - } + "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 }, - "semiotic-mark": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/semiotic-mark/-/semiotic-mark-0.3.0.tgz", - "integrity": "sha512-GxyrIyntvs+TXK8KOJKzs3AnvMM7Cb7ywfAeJKEQ/GKMKwaZvQnuGrz3dSImjfH7xvf4E2AmDIggqgHISt1X4Q==", - "dev": true, - "requires": { - "d3-interpolate": "^1.1.5", - "d3-scale": "^1.0.3", - "d3-selection": "^1.1.0", - "d3-shape": "^1.0.3", - "d3-transition": "^1.0.3", - "prop-types": "^15.6.0", - "roughjs-es5": "0.1.0" - } + "prettier": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.2.tgz", + "integrity": "sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg==", + "dev": true }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true }, - "semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "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 }, - "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=", + "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": { - "sver-compat": "^1.5.0" + "fromentries": "^1.2.0" } }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "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": "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.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - }, - "dependencies": { - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", - "dev": true - } + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" } }, - "serialize-javascript": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.5.0.tgz", - "integrity": "sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ==", - "dev": true - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "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": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" + "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" } }, - "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-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "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=", - "requires": { - "is-extendable": "^0.1.0" - } - } + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - }, - "sha.js": { - "version": "2.4.11", - "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "dev": true, "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" } }, - "shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "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=", + "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": { - "shebang-regex": "^1.0.0" + "side-channel": "^1.1.0" } }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shell-quote": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", - "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "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": { - "array-filter": "~0.0.0", - "array-map": "~0.0.0", - "array-reduce": "~0.0.0", - "jsonify": "~0.0.0" + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" } }, - "shortid": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.8.tgz", - "integrity": "sha1-AzsRfWoul1gE9vCWnb59PQs1UTE=", + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", "dev": true }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "dev": true }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "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 }, - "simple-html-tokenizer": { - "version": "0.1.1", - "resolved": "http://registry.npmjs.org/simple-html-tokenizer/-/simple-html-tokenizer-0.1.1.tgz", - "integrity": "sha1-BcLuxXn//+FFoDCsJs/qYbmA+r4=", + "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 }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "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" + } }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "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": { - "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=", - "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=", - "requires": { - "is-extendable": "^0.1.0" - } - } + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" } }, - "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==", + "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": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.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=", - "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==", - "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==", - "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==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } + "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 } } }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "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": { - "kind-of": "^3.2.0" + "mute-stream": "~0.0.4" + } + }, + "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, + "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": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "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": { - "is-buffer": "^1.1.5" + "safe-buffer": "~5.1.0" } } } }, - "sockjs-client": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.5.tgz", - "integrity": "sha1-G7fA9yIsQPQq3xT0RCy9Eml3GoM=", + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "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" + "picomatch": "^2.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=", + "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": { - "is-plain-obj": "^1.0.0" + "resolve": "^1.20.0" } }, - "sort-keys-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", - "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", + "reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, + "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": { - "sort-keys": "^1.0.0" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" } }, - "source-list-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", - "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } }, - "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==", + "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": { - "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" + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" } }, - "source-map-support": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", - "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", + "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": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "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 + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "replace-homedir": { + "version": "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": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.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 + "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" + } } } }, - "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=" - }, - "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 + "resolve": { + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } }, - "spdx-correct": { + "resolve-cwd": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" + "resolve-from": "^5.0.0" } }, - "spdx-exceptions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", - "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==", + "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, "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" } }, - "spdx-license-ids": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", - "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "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 }, - "split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "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": { - "through": "2" + "value-or-function": "^3.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==", + "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": { - "extend-shallow": "^3.0.0" + "lowercase-keys": "^1.0.0" } }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, - "sshpk": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", - "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "rewiremock": { + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.6.tgz", + "integrity": "sha512-hjpS7iQUTVVh/IHV4GE1ypg4IzlgVc34gxZBarwwVrKfnjlyqHJuQdsia6Ac7m4f4k/zxxA3tX285MOstdysRQ==", + "dev": true, "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", - "tweetnacl": "~0.14.0" + "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" } }, - "ssri": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz", - "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==", + "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": { - "safe-buffer": "^5.1.1" + "glob": "^7.1.3" } }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", - "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.1", - "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.1.tgz", - "integrity": "sha512-Qe8QntFrrpWTnHwvwj2FZTgv+PKIsp0B9VxLzLLbSpPXWOgRgc5LVj/aTiSfK1RqIeF9jeC1UeOH8Q8y60A7og==", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "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": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" + "hash-base": "^3.1.2", + "inherits": "^2.0.4" }, "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=", + "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-descriptor": "^0.1.0" + "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 } } }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", - "dev": true - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true - }, - "stream-browserify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", - "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "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": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" + "queue-microtask": "^1.2.2" } }, - "stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", - "dev": true, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "requires": { - "duplexer": "~0.1.1" + "tslib": "^1.9.0" } }, - "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==", + "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": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" }, "dependencies": { - "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==", - "dev": true, - "requires": { - "once": "^1.4.0" - } + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true } } }, - "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==", + "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 }, - "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==", + "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": { - "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.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://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.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" } }, - "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 + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "streamfilter": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/streamfilter/-/streamfilter-1.0.7.tgz", - "integrity": "sha512-Gk6KZM+yNA1JpW0KzlZIhjo3EaBJDkYfXtYSbOwNIQ7Zd6006E6+sCFlW1NDvFG/vnXhKmw6TJJgiEQg/8lXfQ==", + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "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": { - "readable-stream": "^2.0.2" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" } }, - "streamifier": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/streamifier/-/streamifier-0.1.1.tgz", - "integrity": "sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8=", - "dev": true - }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", - "dev": true + "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": { + "commander": "^2.8.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 + "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": { + "lru-cache": "^6.0.0" + } }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "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": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } + "sver": "^1.8.3" } }, - "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=", + "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": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.0", - "function-bind": "^1.0.2" + "randombytes": "^2.1.0" } }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + "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 }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "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": { - "ansi-regex": "^2.0.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" } }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "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": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + } + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", "dev": true }, - "strip-bom-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz", - "integrity": "sha1-+H217yYT9paKpUWr/h7HKLaoKco=", + "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": { - "first-chunk-stream": "^2.0.0", - "strip-bom": "^2.0.0" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "dependencies": { - "first-chunk-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz", - "integrity": "sha1-G97NuOCDwGZLkZRVgVd6Q6nzHXA=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } + "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 } } }, - "strip-bom-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=", - "dev": true + "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": { + "kind-of": "^6.0.2" + } }, - "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==", + "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": { - "is-natural-number": "^4.0.1" + "shebang-regex": "^1.0.0" } }, - "strip-eof": { + "shebang-regex": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, - "strip-outer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, + "shortid": { + "version": "2.2.17", + "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.17.tgz", + "integrity": "sha512-GpbM3gLF1UUXZvQw6MCyulHkWbRseNO4cyBEZresZRorwl1+SLu1ZdqgVtuwqz8mB6RpwPkm541mYSqrKyJSaA==", "dev": true, "requires": { - "escape-string-regexp": "^1.0.2" + "nanoid": "^3.3.8" } }, - "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==", + "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": { - "loader-utils": "^1.1.0", - "schema-utils": "^1.0.0" + "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" } }, - "styled-jsx": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-3.1.0.tgz", - "integrity": "sha512-drcLtuMC9wKhxZ5C7PyGxy9ADWfw7svB8zemWu+zpG8x4n/hih2xQU2U+SG6HF3TjV3tOjRrNIQOV8vUvffifA==", + "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": { - "babel-plugin-syntax-jsx": "6.18.0", - "babel-types": "6.26.0", - "convert-source-map": "1.5.1", - "loader-utils": "1.1.0", - "source-map": "0.7.3", - "string-hash": "1.1.3", - "stylis": "3.5.3", - "stylis-rule-sheet": "0.0.10" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" } }, - "stylis": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.3.tgz", - "integrity": "sha512-TxU0aAscJghF9I3V9q601xcK3Uw1JbXvpsBGj/HULqexKOKlOEzzlIpLFRbKkCK990ccuxfXUqmPbIIo7Fq/cQ==", - "dev": true - }, - "stylis-rule-sheet": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", - "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==", - "dev": true - }, - "sudo-prompt": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-8.2.0.tgz", - "integrity": "sha512-n5Nv2lIZaWfVBg10EWC8yaJCB6xV7sEsuaISAVFIS9F4fTRjy/O35A82lkweKuSqQItDlKOGQpTHK9/udQhRRw==" - }, - "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 + "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": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } }, - "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=", + "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": { - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" + "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" } }, - "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==", + "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 + }, + "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, + "optional": true + }, + "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": { - "loader-utils": "^0.2.11", - "object-assign": "^4.0.1", - "simple-html-tokenizer": "^0.1.1" + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" }, "dependencies": { - "loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", + "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": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0", - "object-assign": "^4.0.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 } } }, - "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==", - "requires": { - "prop-types": "^15.5.0" - } - }, - "svg-path-bounding-box": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/svg-path-bounding-box/-/svg-path-bounding-box-1.0.4.tgz", - "integrity": "sha1-7XPfODyLR4abZQjwWPV0j4gzwHA=", - "dev": true, - "requires": { - "svgpath": "^2.0.0" - } - }, - "svgo": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", - "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=", + "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": { - "coa": "~1.0.1", - "colors": "~1.1.2", - "csso": "~2.3.1", - "js-yaml": "~3.7.0", - "mkdirp": "~0.5.1", - "sax": "~1.2.1", - "whet.extend": "~0.9.9" + "@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": { - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true }, - "csso": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz", - "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=", - "dev": true, - "requires": { - "clap": "^1.0.9", - "source-map": "^0.5.3" - } + "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": "3.7.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", - "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", + "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": { - "argparse": "^1.0.7", - "esprima": "^2.6.0" + "has-flag": "^4.0.0" } } } }, - "svgpath": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/svgpath/-/svgpath-2.2.1.tgz", - "integrity": "sha1-CDS7Z8iadkcrK9BswQH6e1F7Iiw=", - "dev": true - }, - "symbol-observable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", - "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" - }, - "symbol-tree": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", - "dev": true + "sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "dev": true, + "requires": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + } }, - "tapable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.0.tgz", - "integrity": "sha512-IlqtmLVaZA2qab8epUXbVWRn3aB1imbDMJtjB3nu4X0NqPkcY/JH9ZtCBWKHWPxs8Svi9tyo8w2dBoi07qZbBA==", + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "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": { - "block-stream": "*", - "fstream": "^1.0.2", - "inherits": "2" + "is-plain-obj": "^1.0.0" } }, - "tar-stream": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", - "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", + "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": { - "bl": "^1.0.0", - "buffer-alloc": "^1.1.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.0", - "xtend": "^4.0.0" + "sort-keys": "^1.0.0" }, "dependencies": { - "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==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "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==", + "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": { - "safe-buffer": "~5.1.0" + "is-plain-obj": "^1.0.0" } } } }, - "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 - }, - "throttleit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=" - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "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 }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "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": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" - }, - "dependencies": { - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "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" - } - } + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "through2-filter": { + "sparkles": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", + "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", + "dev": true + }, + "spawn-wrap": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", - "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", "dev": true, "requires": { - "through2": "~2.0.0", - "xtend": "~4.0.0" + "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" } }, - "time-stamp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "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 }, - "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==", + "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": { - "setimmediate": "^1.0.4" + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" } }, - "timers-ext": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.5.tgz", - "integrity": "sha512-tsEStd7kmACHENhsUPaxb8Jf8/+GZZxyNFQbZD07HQOyooOa6At1rQqjffgvg7n+dxscQa9cjjMdWhJtsP2sxg==", + "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": { - "es5-ext": "~0.10.14", - "next-tick": "1" + "streamx": "^2.13.2" } }, - "tiny-emitter": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz", - "integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==", - "dev": true, - "optional": true - }, - "tinycolor2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", - "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=", - "dev": true - }, - "tinyqueue": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz", - "integrity": "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==", + "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 }, - "tmp": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", - "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", - "requires": { - "os-tmpdir": "~1.0.1" - } - }, - "to-absolute-glob": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", - "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "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": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.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" } }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "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 }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", - "dev": true + "streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "dev": true, + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + } }, - "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=", + "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 }, - "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=", + "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": { - "kind-of": "^3.0.2" + "safe-buffer": "~5.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=", - "requires": { - "is-buffer": "^1.1.5" - } + "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-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "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": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, - "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=", + "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": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, - "to-through": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", - "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", + "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": { - "through2": "^2.0.3" + "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" } }, - "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==", + "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": { - "nopt": "~1.0.10" - }, - "dependencies": { - "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" - } - } + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" } }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "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": { - "punycode": "^1.4.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "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": { - "punycode": "^2.1.0" - }, - "dependencies": { - "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 - } + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, - "tree-kill": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.0.tgz", - "integrity": "sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg==" - }, - "trim": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", - "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", - "dev": true + "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": { + "ansi-regex": "^5.0.1" + } }, - "trim-repeated": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", + "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": { - "escape-string-regexp": "^1.0.2" + "ansi-regex": "^5.0.1" } }, - "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 + "strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "requires": { + "is-natural-number": "^4.0.1" + } }, - "trim-trailing-lines": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.1.tgz", - "integrity": "sha512-bWLv9BbWbbd7mlqqs2oQYnLD/U/ZqeJeJwbO0FG2zA1aTq+HTvxfHNKFa/HGCVyJpDiioUYaBhfiT6rgk+l4mg==", + "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 }, - "trough": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.3.tgz", - "integrity": "sha512-fwkLWH+DimvA4YCy+/nvJd61nWQQ2liO/nF/RjkTpiOGi+zxZzVkhb1mvbHIIW4b/8nDsYI8uTmAlc0nNkRMOw==", + "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 }, - "tryer": { + "strip-outer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", - "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", - "dev": true - }, - "ts-loader": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-5.3.0.tgz", - "integrity": "sha512-lGSNs7szRFj/rK9T1EQuayE3QNLg6izDUxt5jpmq0RG1rU2bapAt7E7uLckLCUPeO1jwxCiet2oRaWovc53UAg==", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", "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" - }, - "dependencies": { - "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" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - } + "escape-string-regexp": "^1.0.2" } }, - "ts-mockito": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.3.1.tgz", - "integrity": "sha512-chcKw0sTApwJxTyKhzbWxI4BTUJ6RStZKUVh2/mfwYqFS09PYy5pvdXZwG35QSkqT5pkdXZlYKBX196RRvEZdQ==", + "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": { - "lodash": "^4.17.5" + "has-flag": "^3.0.0" } }, - "tsconfig-paths": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.7.0.tgz", - "integrity": "sha512-7iE+Q/2E1lgvxD+c0Ot+GFFmgmfIjt/zCayyruXkXQ84BLT85gHXy0WSoQSiuFX9+d+keE/jiON7notV74ZY+A==", + "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": { - "@types/json5": "^0.0.29", - "deepmerge": "^2.0.1", - "json5": "^1.0.1", - "minimist": "^1.2.0", - "strip-bom": "^3.0.0" + "semver": "^6.3.0" }, "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "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": { - "minimist": "^1.2.0" - } + "optional": true } } }, - "tsconfig-paths-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.2.0.tgz", - "integrity": "sha512-S/gOOPOkV8rIL4LurZ1vUdYCVgo15iX9ZMJ6wx6w2OgcpT/G4wMyHB6WM+xheSqGMrWKuxFul+aXpCju3wmj/g==", + "tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true + }, + "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": { - "chalk": "^2.3.0", - "enhanced-resolve": "^4.0.0", - "tsconfig-paths": "^3.4.0" + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" }, "dependencies": { - "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==", + "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": { - "color-convert": "^1.9.0" + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" } }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "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": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "end-of-stream": "^1.1.0", + "once": "^1.3.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 + "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" + } }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "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": { - "has-flag": "^3.0.0" + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" } } } }, - "tslib": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.1.tgz", - "integrity": "sha512-avfPS28HmGLLc2o4elcc2EIq2FcH++Yo5YxpBZi9Yw93BCTGFthI4HPE4Rpep6vSYQaK8e69PelM44tPj+RaQg==", - "dev": true + "tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + } + }, + "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": { + "streamx": "^2.12.5" + } }, - "tslint": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.10.0.tgz", - "integrity": "sha1-EeJrzLiK+gLdDZlWyuPUVAtfVMM=", + "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": { - "babel-code-frame": "^6.22.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^3.2.0", - "glob": "^7.1.1", - "js-yaml": "^3.7.0", - "minimatch": "^3.0.4", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.8.0", - "tsutils": "^2.12.1" + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + } + }, + "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": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" }, "dependencies": { - "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==", + "ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" } }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "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": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "fast-deep-equal": "^3.1.3" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "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 }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "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": { - "has-flag": "^3.0.0" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" } } } }, - "tslint-eslint-rules": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/tslint-eslint-rules/-/tslint-eslint-rules-5.3.1.tgz", - "integrity": "sha512-qq2H/AU/FlFbQJKXuxhtIk+ni/nQu9jHHhsFKa6hnA0/n3zl1/RWRc3TVFlL8HfWFMzkST350VeTrFpy1u4OUg==", + "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": { - "doctrine": "0.7.2", - "tslib": "1.9.0", - "tsutils": "2.8.0" + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" }, "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": "2.8.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.8.0.tgz", - "integrity": "sha1-AWAXNymzvxOGKN0UoVN+AIUdgUo=", + "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.7.1" + "brace-expansion": "^1.1.7" } } } }, - "tslint-microsoft-contrib": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/tslint-microsoft-contrib/-/tslint-microsoft-contrib-5.0.3.tgz", - "integrity": "sha512-5AnfTGlfpUzpRHLmoojPBKFTTmbjnwgdaTHMdllausa4GBPya5u36i9ddrTX4PhetGZvd4JUYIpAmgHqVnsctg==", + "text-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", "dev": true, "requires": { - "tsutils": "^2.12.1" + "b4a": "^1.6.4" } }, - "tsutils": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz", - "integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==", + "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 + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "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": { - "tslib": "^1.8.1" + "readable-stream": "~2.3.6", + "xtend": "~4.0.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-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "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": { - "safe-buffer": "^5.0.1" + "through2": "~2.0.0", + "xtend": "~4.0.0" } }, - "tv4": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", - "integrity": "sha1-0CDIRvrdUMhVq7JeuuzGj8EPeWM=", + "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 }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "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": { - "prelude-ls": "~1.1.2" + "setimmediate": "^1.0.4" } }, - "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 + "tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==" }, - "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "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": { - "media-typer": "0.3.0", - "mime-types": "~2.1.18" + "is-absolute": "^1.0.0", + "is-negated-glob": "^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=", + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "dev": true, "requires": { - "@types/react": "^0.14.44" + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" }, "dependencies": { - "@types/react": { - "version": "0.14.57", - "resolved": "http://registry.npmjs.org/@types/react/-/react-0.14.57.tgz", - "integrity": "sha1-GHioZU+v3R04G4RXKStkM0mMW2I=", + "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 } } }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true + "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" + } }, - "typemoq": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", - "integrity": "sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==", + "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": { - "circular-json": "^0.3.1", - "lodash": "^4.17.4", - "postinstall-build": "^5.0.1" + "through2": "^2.0.3" } }, - "typescript": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.2.tgz", - "integrity": "sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg==", + "totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", "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=" + "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": { + "escape-string-regexp": "^1.0.2" + } }, - "typescript-formatter": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/typescript-formatter/-/typescript-formatter-7.2.0.tgz", - "integrity": "sha512-A16UqkHtkQOF340cf21LJXchcftyBTPqNOAmP1J8Plu2m3Q8o+2fAYwgFjLXMKP6ooSPjDoOS6z8j9q+1nEnXg==", + "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": { - "commandpost": "^1.0.0", - "editorconfig": "^0.15.0" + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" }, "dependencies": { - "editorconfig": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.0.tgz", - "integrity": "sha512-j7JBoj/bpNzvoTQylfRZSc85MlLNKWQiq5y6gwKhmqD2h1eZ+tH4AXbkhEJD468gjDna/XMx2YtSkCxBRX9OGg==", + "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": { - "@types/commander": "^2.11.0", - "@types/semver": "^5.4.0", - "commander": "^2.11.0", - "lru-cache": "^4.1.1", - "semver": "^5.4.1", - "sigmund": "^1.0.1" + "color-convert": "^2.0.1" } }, - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "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": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "has-flag": "^4.0.0" } } } }, - "ua-parser-js": { - "version": "0.7.18", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.18.tgz", - "integrity": "sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA==", - "dev": true + "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": { + "lodash": "^4.17.5" + } }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "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" + } + }, + "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, - "optional": true, "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" }, "dependencies": { - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true, - "optional": true - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "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, - "optional": true, "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" + "minimist": "^1.2.0" } }, - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true, - "optional": true - }, - "yargs": { - "version": "3.10.0", - "resolved": "http://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "optional": true, - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.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 } } }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "dev": true, - "optional": true - }, - "uglifyjs-webpack-plugin": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz", - "integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==", + "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": { - "cacache": "^10.0.4", - "find-cache-dir": "^1.0.0", - "schema-utils": "^0.4.5", - "serialize-javascript": "^1.4.0", - "source-map": "^0.6.1", - "uglify-es": "^3.3.4", - "webpack-sources": "^1.1.0", - "worker-farm": "^1.5.2" + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tsconfig-paths": "^3.9.0" }, "dependencies": { - "ajv": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.4.tgz", - "integrity": "sha512-4Wyjt8+t6YszqaXnLDfMmG/8AlO5Zbcsy3ATHncCzjW/NoPzAId8AK6749Ybjmdt+kUY1gP60fCu46oDxPv/mg==", + "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": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "color-convert": "^2.0.1" } }, - "commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", - "dev": true + "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" + } }, - "fast-deep-equal": { + "color-convert": { "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=", - "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 - }, - "schema-utils": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", - "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" + "color-name": "~1.1.4" } }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "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 }, - "uglify-es": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", - "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", + "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": { - "commander": "~2.13.0", - "source-map": "~0.6.1" + "has-flag": "^4.0.0" } } } }, - "uint64be": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uint64be/-/uint64be-1.0.1.tgz", - "integrity": "sha1-H3FUIC8qG4rzU4cd2mUb80zpPpU=" + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" }, - "ultron": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", - "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=" + "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 }, - "unbzip2-stream": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.2.5.tgz", - "integrity": "sha512-izD3jxT8xkzwtXRUZjtmRwKnZoeECrfZ8ra/ketwOcusbZEp4mjULMnJOCfTDZBgGQAAY1AJ/IgxcwkavcX9Og==", + "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": { - "buffer": "^3.0.1", - "through": "^2.3.6" + "safe-buffer": "^5.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.0", - "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.0.tgz", - "integrity": "sha1-M52kZGJS0ILcN45wgGcpl1DhG0k=", + "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": { - "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" + "prelude-ls": "^1.2.1" } }, - "undertaker-registry": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", - "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", + "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 }, - "unherit": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.1.tgz", - "integrity": "sha512-+XZuV691Cn4zHsK0vkKYwBEwB74T3IZIcxrgn2E4rKwTfFyI1zCh7X7grwh9Re08fdPlarIdyWgI8aVB3F5A5g==", + "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": { - "inherits": "^2.0.1", - "xtend": "^4.0.1" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" } }, - "unicode": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/unicode/-/unicode-10.0.0.tgz", - "integrity": "sha1-5dUcHbk7bHGguHngsMSvfm/faI4=" - }, - "unicode-canonical-property-names-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", - "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "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": { - "unicode-canonical-property-names-ecmascript": "^1.0.4", - "unicode-property-aliases-ecmascript": "^1.0.4" + "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" } }, - "unicode-match-property-value-ecmascript": { + "typed-array-byte-offset": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz", - "integrity": "sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz", - "integrity": "sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==", - "dev": true - }, - "unified": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz", - "integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "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" + "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" } }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "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": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } + "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" } }, - "uniqid": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-5.0.3.tgz", - "integrity": "sha512-R2qx3X/LYWSdGRaluio4dYrPXAJACTqyUjuyXHoJLBUOIfmMcnYOyY2d6Y4clZcIz5lK6ZaI0Zzmm0cPfsIqzQ==", - "dev": true - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "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": { - "unique-slug": "^2.0.0" + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" } }, - "unique-slug": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.1.tgz", - "integrity": "sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg==", + "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": { - "imurmurhash": "^0.1.4" + "is-typedarray": "^1.0.0" } }, - "unique-stream": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", - "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", + "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": { - "json-stable-stringify": "^1.0.0", - "through2-filter": "^2.0.0" + "circular-json": "^0.3.1", + "lodash": "^4.17.4", + "postinstall-build": "^5.0.1" } }, - "unist-util-is": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-2.1.2.tgz", - "integrity": "sha512-YkXBK/H9raAmG7KXck+UUpnKiNmUdB+aBGrknfQ4EreE1banuzrKABx3jP6Z5Z3fMSPMQQmeXBlKpCbMwBkxVw==", + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true }, - "unist-util-remove-position": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.2.tgz", - "integrity": "sha512-XxoNOBvq1WXRKXxgnSYbtCF76TJrRoe5++pD4cCBsssSiWSnPEktyFrFLE8LTk3JW5mt9hB0Sk5zn4x/JeWY7Q==", + "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==" + }, + "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": { - "unist-util-visit": "^1.1.0" + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" } }, - "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.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.0.tgz", - "integrity": "sha512-FiGu34ziNsZA3ZUteZxSFaczIjGmksfSgdKqBfOejrrfzyUy5b7YrlzT1Bcvi+djkYDituJDy2XB7tGTeBieKw==", + "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": { - "unist-util-visit-parents": "^2.0.0" - }, - "dependencies": { - "unist-util-visit-parents": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.0.1.tgz", - "integrity": "sha512-6B0UTiMfdWql4cQ03gDTCSns+64Zkfo2OCbK31Ov0uMizEz+CJeAp0cgZVb5Fhmcd7Bct2iRNywejT0orpbqUA==", - "dev": true, - "requires": { - "unist-util-is": "^2.1.2" - } - } + "buffer": "^5.2.1", + "through": "^2.3.8" } }, - "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==", + "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 }, - "universalify": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "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 }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "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": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" + "bach": "^2.0.1", + "fast-levenshtein": "^3.0.0", + "last-run": "^2.0.0", + "undertaker-registry": "^2.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=", + "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": { - "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=", - "requires": { - "isarray": "1.0.0" - } - } + "fastest-levenshtein": "^1.0.7" } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" } } }, - "untildify": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.2.tgz", - "integrity": "sha1-fx8wIFWz/qDz6B3HjrNnZstl4/E=" - }, - "upath": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", - "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", + "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 }, - "upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", + "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 }, + "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": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "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": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + } + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -16834,21 +26309,8 @@ "dev": true, "requires": { "punycode": "^2.1.0" - }, - "dependencies": { - "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 - } } }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, "url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", @@ -16867,246 +26329,147 @@ } } }, - "url-loader": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.1.tgz", - "integrity": "sha512-vugEeXjyYFBCUOpX+ZuaunbK3QXMKaQ3zUnRfIpRBlGkY7QizCnzyyn2ASfcxsvyU3ef+CJppVywnl3Kgf13Gg==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "mime": "^2.0.3", - "schema-utils": "^1.0.0" - } - }, - "url-parse": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz", - "integrity": "sha512-rh+KuAW36YKo0vClhQzLLveoj8FwPJNu65xLb7Mrt+eZht0IPT0IXgSv8gcMegZ6NvjJUALf6Mf25POlMwD1Fw==", - "requires": { - "querystringify": "^2.0.0", - "requires-port": "^1.0.0" - } + "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": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", - "dev": true, - "requires": { - "prepend-http": "^2.0.0" - } - }, - "url-to-options": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", - "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", - "dev": true - }, - "urlgrey": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/urlgrey/-/urlgrey-0.4.4.tgz", - "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=", - "dev": true - }, - "urllite": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/urllite/-/urllite-0.5.0.tgz", - "integrity": "sha1-G3u5yj+w25Ug3hE0ZrvPfMNBRRo=", - "requires": { - "xtend": "~4.0.0" - } - }, - "use": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", - "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", + "dev": true, "requires": { - "kind-of": "^6.0.2" + "prepend-http": "^2.0.0" } }, + "url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==", + "dev": true + }, "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", "dev": true, "requires": { "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } } }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", - "dev": true - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "dev": true }, - "v8-compile-cache": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz", - "integrity": "sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw==", + "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 }, "v8flags": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.1.tgz", - "integrity": "sha512-iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "vali-date": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", - "integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", "dev": true }, - "validate-npm-package-license": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", - "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "validator": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", - "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==" - }, "value-or-function": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", "dev": true }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vfile": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz", - "integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==", - "dev": true, - "requires": { - "is-buffer": "^1.1.4", - "replace-ext": "1.0.0", - "unist-util-stringify-position": "^1.0.0", - "vfile-message": "^1.0.0" - }, - "dependencies": { - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - } - } - }, - "vfile-location": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.3.tgz", - "integrity": "sha512-zM5/l4lfw1CBoPx3Jimxoc5RNDAHHpk6AM6LM0pTIkm5SUSsx8ZekZ0PVdf0WEZ7kjlhSt7ZlqbRL6Cd6dBs6A==", - "dev": true - }, - "vfile-message": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.0.1.tgz", - "integrity": "sha512-vSGCkhNvJzO6VcWC6AlJW4NtYOVtS+RgCaqFIYUjoGIlHnFL+i0LbtYvonDWOMcB97uTPT4PRsyYY7REWC9vug==", - "dev": true, - "requires": { - "unist-util-stringify-position": "^1.1.1" - } - }, "vinyl": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", - "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", + "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": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" + "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" } }, - "vinyl-file": { + "vinyl-contents": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-2.0.0.tgz", - "integrity": "sha1-p+v1/779obfRjRQPyweyI++2dRo=", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.3.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0", - "strip-bom-stream": "^2.0.0", - "vinyl": "^1.1.0" + "bl": "^5.0.0", + "vinyl": "^3.0.0" }, "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true + "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" + } }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "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": { - "is-utf8": "^0.2.0" + "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": "1.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "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": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" } } } @@ -17134,98 +26497,6 @@ "value-or-function": "^3.0.0", "vinyl": "^2.0.0", "vinyl-sourcemap": "^1.1.0" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "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 - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://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" - } - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "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": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "vinyl-source-stream": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vinyl-source-stream/-/vinyl-source-stream-1.1.2.tgz", - "integrity": "sha1-YrU6E1YQqJbpjKlr7jqH8Aio54A=", - "dev": true, - "requires": { - "through2": "^2.0.3", - "vinyl": "^0.4.3" - }, - "dependencies": { - "clone": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", - "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", - "dev": true - }, - "vinyl": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", - "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", - "dev": true, - "requires": { - "clone": "^0.2.0", - "clone-stats": "^0.0.1" - } - } } }, "vinyl-sourcemap": { @@ -17243,525 +26514,280 @@ "vinyl": "^2.0.0" }, "dependencies": { - "clone": { + "normalize-path": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", - "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", - "dev": true - }, - "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 - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "vinyl": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", - "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" + "remove-trailing-separator": "^1.0.1" } } } }, - "viz-annotation": { - "version": "0.0.1-3", - "resolved": "https://registry.npmjs.org/viz-annotation/-/viz-annotation-0.0.1-3.tgz", - "integrity": "sha512-jZSnuAsfu3MKGa2vAShzw3oUG71tfVmk0DQvYG/YbQ1Kpc5AlU0v2lgHekO2nVPvIiM6mWrfIths4IZBYTh0xQ==", + "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 }, - "vm-browserify": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", - "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", - "dev": true, - "requires": { - "indexof": "0.0.1" - } + "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": { - "version": "1.1.22", - "resolved": "https://registry.npmjs.org/vscode/-/vscode-1.1.22.tgz", - "integrity": "sha512-G/zu7PRAN1yF80wg+l6ebIexDflU3uXXeabacJuLearTIfObKw4JaI8aeHwDEmpnCkc3MkIr3Bclkju2gtEz6A==", - "dev": true, + "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==" + }, + "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==", "requires": { - "glob": "^7.1.2", - "gulp-chmod": "^2.0.0", - "gulp-filter": "^5.0.1", - "gulp-gunzip": "1.0.0", - "gulp-remote-src-vscode": "^0.5.1", - "gulp-symdest": "^1.1.1", - "gulp-untar": "^0.0.7", - "gulp-vinyl-zip": "^2.1.2", - "mocha": "^4.0.1", - "request": "^2.83.0", - "semver": "^5.4.1", - "source-map-support": "^0.5.0", - "url-parse": "^1.4.3", - "vinyl-source-stream": "^1.1.0" + "minimatch": "^9.0.3", + "semver": "^7.6.0", + "vscode-languageserver-protocol": "3.17.6-next.10" }, "dependencies": { - "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", - "dev": true - }, - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", - "dev": true - }, - "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" - } - }, - "diff": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", - "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", - "dev": true - }, - "growl": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", - "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", - "dev": true - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "mocha": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", - "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", - "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": { - "browser-stdout": "1.3.0", - "commander": "2.11.0", - "debug": "3.1.0", - "diff": "3.3.1", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.3", - "he": "1.1.1", - "mkdirp": "0.5.1", - "supports-color": "4.4.0" + "balanced-match": "^1.0.0" } }, - "supports-color": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", - "dev": true, + "minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "requires": { - "has-flag": "^2.0.0" + "brace-expansion": "^2.0.2" } } } }, - "vscode-debugadapter": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.28.0.tgz", - "integrity": "sha512-GCR1326LFtfYjl7SDN1wmU2pBJ98HgUCnbWoU3s3bz0GhUWYN1xSYGg7MfuwxY6WwZk2cuqzANhy/oaKADMXaw==", - "requires": { - "vscode-debugprotocol": "1.28.0", - "vscode-uri": "1.0.1" - } - }, - "vscode-debugadapter-testsupport": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/vscode-debugadapter-testsupport/-/vscode-debugadapter-testsupport-1.29.0.tgz", - "integrity": "sha512-4P0h3gfe7MNw9FXx0k/TpKBJMA4s880Gu+puxuOHOY3txYpIGC1I2jQdPlWt0XPWK2Qcz7q/biQ221cfavqifw==", - "dev": true, - "requires": { - "vscode-debugprotocol": "1.29.0" - }, - "dependencies": { - "vscode-debugprotocol": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.29.0.tgz", - "integrity": "sha512-jrbSayWof7jyXo7VRhIcTcsjWeiPloi6vzbrucVarKvuSrZUV7Bc+ggQRSG1lzNiMmBG5AHIe/Npf6G2q4SBiw==", - "dev": true - } - } - }, - "vscode-debugprotocol": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.28.0.tgz", - "integrity": "sha512-QM4J8A13jBY9I7OPWXN0ZO1cqydnD4co2j/O81jIj6em8VkmJT4VyJQkq4HmwJe3af+u9+7IYCIEDrowgvKxTA==" - }, - "vscode-extension-telemetry": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.0.tgz", - "integrity": "sha512-WVCnP+uLxlqB6UD98yQNV47mR5Rf79LFxpuZhSPhEf0Sb4tPZed3a63n003/dchhOwyCTCBuNN4n8XKJkLEI1Q==", - "requires": { - "applicationinsights": "1.0.6" - } - }, - "vscode-jsonrpc": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-3.6.2.tgz", - "integrity": "sha512-T24Jb5V48e4VgYliUXMnZ379ItbrXgOimweKaJshD84z+8q7ZOZjJan0MeDe+Ugb+uqERDVV8SBmemaGMSMugA==" - }, - "vscode-languageclient": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-4.4.0.tgz", - "integrity": "sha512-sXBwIcwG4W5MjnDAfXf0hM5ErOcXxEBlix6QJb5ijf0gtecYygrMAqv8hag7sEg/jCCOKQdXJ4K1iZL3GZcJZg==", - "requires": { - "vscode-languageserver-protocol": "^3.10.0" - } - }, - "vscode-languageserver": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-4.4.0.tgz", - "integrity": "sha512-NO4JQg286YLSdU11Fko6cke19kwSob3O0bhf6xDxIJuDhUbFy0VEPRB5ITc3riVmp13+Ki344xtqJYmqfcmCrg==", - "requires": { - "vscode-languageserver-protocol": "^3.10.0", - "vscode-uri": "^1.0.3" - }, - "dependencies": { - "vscode-uri": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.6.tgz", - "integrity": "sha512-sLI2L0uGov3wKVb9EB+vIQBl9tVP90nqRvxSoJ35vI3NjxE8jfsE5DSOhWgSunHSZmKS4OCi2jrtfxK7uyp2ww==" - } - } - }, "vscode-languageserver-protocol": { - "version": "3.10.3", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.10.3.tgz", - "integrity": "sha512-R9hKsmXmpIXBLpy6I0eztfAcWU0KHr1lADJiJq+VCmdiHGVUJugMIvU6qVCzLP9wRtZ02AF98j09NAKq10hWeQ==", + "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": "^3.6.2", - "vscode-languageserver-types": "^3.10.1" + "vscode-jsonrpc": "9.0.0-next.5", + "vscode-languageserver-types": "3.17.6-next.5" } }, "vscode-languageserver-types": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.10.1.tgz", - "integrity": "sha512-HeQ1BPYJDly4HfKs0h2TUAZyHfzTAhgQsCwsa1tW9PhuvGGsd2r3Q53FFVugwP7/2bUv3GWPoTgAuIAkIdBc4w==" - }, - "vscode-uri": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.1.tgz", - "integrity": "sha1-Eahr7+rDxKo+wIYjZRo8gabQu8g=" + "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==" }, - "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, + "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==", "requires": { - "browser-process-hrtime": "^0.1.2" + "tas-client": "0.2.33" } }, "watchpack": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", - "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", - "dev": true, - "requires": { - "chokidar": "^2.0.2", - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "chokidar": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", - "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.2.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "lodash.debounce": "^4.0.8", - "normalize-path": "^2.1.1", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.5" - } - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - } + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "requires": { + "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.20.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.20.2.tgz", - "integrity": "sha512-75WFUMblcWYcocjSLlXCb71QuGyH7egdBZu50FtBGl2Nso8CK3Ej+J7bTZz2FPFq5l6fzCisD9modB7t30ikuA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.7.8", - "@webassemblyjs/helper-module-context": "1.7.8", - "@webassemblyjs/wasm-edit": "1.7.8", - "@webassemblyjs/wasm-parser": "1.7.8", - "acorn": "^5.6.2", - "acorn-dynamic-import": "^3.0.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": "^0.4.4", - "tapable": "^1.1.0", - "uglifyjs-webpack-plugin": "^1.2.4", - "watchpack": "^1.5.0", - "webpack-sources": "^1.3.0" - }, - "dependencies": { - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", - "dev": true - }, + "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": "6.5.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.4.tgz", - "integrity": "sha512-4Wyjt8+t6YszqaXnLDfMmG/8AlO5Zbcsy3ATHncCzjW/NoPzAId8AK6749Ybjmdt+kUY1gP60fCu46oDxPv/mg==", + "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": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" } }, - "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=", - "dev": true + "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": { + "fast-deep-equal": "^3.1.3" + } }, "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==", + "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": "0.4.7", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", - "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", + "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": { - "ajv": "^6.1.0", - "ajv-keywords": "^3.1.0" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" } } } }, "webpack-bundle-analyzer": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.0.3.tgz", - "integrity": "sha512-naLWiRfmtH4UJgtUktRTLw6FdoZJ2RvCR9ePbwM9aRMsS/KjFerkPZG9epEvXRAw5d5oPdrs9+3p+afNjxW8Xw==", - "dev": true, - "requires": { - "acorn": "^5.7.3", - "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.10", - "mkdirp": "^0.5.1", - "opener": "^1.5.1", - "ws": "^6.0.0" - }, - "dependencies": { - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", - "dev": true - }, + "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": { + "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": { "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==", + "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": "^1.9.0" + "color-convert": "^2.0.1" } }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "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": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", - "dev": true - }, - "filesize": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", - "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==", - "dev": true - }, - "gzip-size": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.0.0.tgz", - "integrity": "sha512-5iI7omclyqrnWw4XbXAmGhPsABkSIDQonv2K0h61lybgofWa6iZyvrI3r2zsJH4P8Nb64fFVzlvfhs0g7BBxAA==", + "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": { - "duplexer": "^0.1.1", - "pify": "^3.0.0" + "color-name": "~1.1.4" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "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 }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "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 }, - "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" - } + "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 }, - "ws": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.0.tgz", - "integrity": "sha512-H3dGVdGvW2H8bnYpIDc3u3LH8Wue3Qh+Zto6aXXFzvESkTVT6rAfKR6tR/+coaUvxs8yHtmNV0uioBF62ZGSTg==", + "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": { - "async-limiter": "~1.0.0" + "has-flag": "^4.0.0" } } } }, "webpack-cli": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.1.2.tgz", - "integrity": "sha512-Cnqo7CeqeSvC6PTdts+dywNi5CRlIPbLx1AoUPK2T6vC1YAugMG3IOoO9DmEscd+Dghw7uRlnzV1KwOe5IrtgQ==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "enhanced-resolve": "^4.1.0", - "global-modules-path": "^2.3.0", - "import-local": "^2.0.0", - "interpret": "^1.1.0", - "loader-utils": "^1.1.0", - "supports-color": "^5.5.0", - "v8-compile-cache": "^2.0.2", - "yargs": "^12.0.2" + "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": { - "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" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } + "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 }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "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 }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "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": { - "has-flag": "^3.0.0" + "resolve": "^1.9.0" } } } @@ -17772,234 +26798,185 @@ "integrity": "sha1-iCuOTRqpPEjLj9r4Rvx52G+C8U8=", "dev": true }, - "webpack-log": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-1.2.0.tgz", - "integrity": "sha512-U9AnICnu50HXtiqiDxuli5gLB5PGBo7VvcHx36jRZHwK4vzOYLbImqT4lwWwoMHdQWwEKw736fCHEekokTEKHA==", - "dev": true, - "requires": { - "chalk": "^2.1.0", - "log-symbols": "^2.1.0", - "loglevelnext": "^1.0.1", - "uuid": "^3.1.0" - }, - "dependencies": { - "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" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" - } - } - } - }, "webpack-merge": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.4.tgz", - "integrity": "sha512-TmSe1HZKeOPey3oy1Ov2iS3guIZjWvMT2BBJDzzT5jScHTjVC3mpjJofgueEzaEd6ibhxRDD6MIblDr8tzh8iQ==", + "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": "http://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-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" - }, - "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 - } - } - }, - "websocket-driver": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", - "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "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": { - "http-parser-js": ">=0.4.0", - "websocket-extensions": ">=0.1.1" - } + "requires": {} }, - "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "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-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, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "requires": { - "iconv-lite": "0.4.24" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } + "isexe": "^2.0.0" } }, - "whatwg-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", - "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==", - "dev": true - }, - "whatwg-mimetype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz", - "integrity": "sha512-5YSO1nMd5D1hY3WzAQV3PzZL83W3YeyR1yW9PcH26Weh1t+Vzh9B6XkDh7aXm83HBZ4nSMvkjvN2H2ySWIvBgw==", - "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==", + "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": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.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" } }, - "whet.extend": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", - "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "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": { - "isexe": "^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" } }, - "which-module": { + "wildcard": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true, - "optional": true - }, "winreg": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz", "integrity": "sha1-ugZWKbepJRMOFXeRCM9UCZDpjRs=" }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "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 }, - "worker-farm": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz", - "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==", + "wipe-webpack-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wipe-webpack-cache/-/wipe-webpack-cache-2.1.0.tgz", + "integrity": "sha512-OXzQMGpA7MnQQ8AG+uMl5mWR2ezy6fw1+DMHY+wzYP1qkF1jrek87psLBmhZEj+er4efO/GD4R8jXWFierobaA==", + "dev": true, + "requires": { + "wipe-node-cache": "^2.1.0" + } + }, + "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": { - "errno": "~0.1.7" + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" } }, + "workerpool": { + "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": "http://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": { - "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=", + "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": { - "number-is-nan": "^1.0.0" + "color-convert": "^2.0.1" } }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "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": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.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 } } }, - "wrapper-webpack-plugin": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wrapper-webpack-plugin/-/wrapper-webpack-plugin-2.0.0.tgz", - "integrity": "sha512-HiykPJTuiaPiR9Q89sRbTjWJ9J/AkriPTbIYaAAW5ulfaK7p5GqK9cB+RWwFhfa17Sn5ehqJ2/qxF4XbQCDGvg==", + "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": { - "webpack-sources": "^1.1.0" + "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" + } + }, + "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 + } } }, "wrappy": { @@ -18007,20 +26984,24 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, - "ws": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", - "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", + "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": { - "options": ">=0.0.5", - "ultron": "1.0.x" + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" } }, - "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 + "ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "requires": {} }, "xml": { "version": "1.0.1", @@ -18028,180 +27009,195 @@ "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" + "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": "1.3.1", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-1.3.1.tgz", - "integrity": "sha512-tGkGJkN8XqCod7OT+EvGYK5Z4SfDQGD30zAa58OcnAa0RRWgzUEK72tkXhsX1FZd+rgnhRxFtmO+ihkp8LHSkw==", - "dev": true - }, - "xmlhttprequest": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", - "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" - }, - "xregexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", - "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==", - "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.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + "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==", + "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=", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.2.tgz", - "integrity": "sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ==", + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", "dev": true, "requires": { - "cliui": "^4.0.0", - "decamelize": "^2.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", + "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": "^1.0.1", + "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^2.0.0", + "string-width": "^4.2.0", "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^10.1.0" + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" }, "dependencies": { - "decamelize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", - "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", + "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": { - "xregexp": "4.0.0" + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" } }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" } }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "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": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "color-name": "~1.1.4" } }, - "p-limit": { + "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 + }, + "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 + }, + "which-module": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", - "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", + "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": { - "p-try": "^2.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.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==", + "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": { - "p-limit": "^2.0.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } - }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "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 } } }, "yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "requires": { - "camelcase": "^4.1.0" + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" }, "dependencies": { "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", + "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", + "dev": true + }, + "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 + }, + "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 } } }, "yauzl": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.9.1.tgz", - "integrity": "sha1-qBmB6nCleUYTOIPwKcWCGok1mn8=", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, "requires": { "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.0.1" + "fd-slicer": "~1.1.0" } }, "yazl": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.0.tgz", - "integrity": "sha512-rgptqKwX/f1/7bIRF1FHb4HGsP5k11QyxBpDl1etUDfNpTa7CNjDOYNPFnIaEzZ9dRq0c47IEJS+sy+T39JCLw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", "dev": true, "requires": { "buffer-crc32": "~0.2.3" } }, - "zone.js": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.7.6.tgz", - "integrity": "sha1-+7w50+AmHQmG8boGMG6zrrDSIAk=" + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "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 b53dd7603a88..000000000000 --- a/package.datascience-ui.dependencies.json +++ /dev/null @@ -1,146 +0,0 @@ -[ - "@babel/runtime-corejs2", - "@emotion/hash", - "@emotion/memoize", - "@emotion/stylis", - "@emotion/unitless", - "@mapbox/polylabel", - "@nteract/markdown", - "@nteract/mathjax", - "@nteract/octicons", - "@nteract/plotly", - "@nteract/transform-dataresource", - "@nteract/transform-geojson", - "@nteract/transform-model-debug", - "@nteract/transform-plotly", - "@nteract/transform-vdom", - "@nteract/transforms", - "anser", - "ansi-to-html", - "ansi-to-react", - "babel-polyfill", - "babel-runtime", - "bail", - "base16", - "bintrees", - "character-entities-legacy", - "character-reference-invalid", - "classnames", - "collapse-white-space", - "core-js", - "create-emotion", - "css-loader", - "d3-array", - "d3-bboxCollide", - "d3-brush", - "d3-chord", - "d3-collection", - "d3-color", - "d3-contour", - "d3-dispatch", - "d3-drag", - "d3-ease", - "d3-force", - "d3-format", - "d3-glyphedge", - "d3-hexbin", - "d3-hierarchy", - "d3-interpolate", - "d3-path", - "d3-polygon", - "d3-quadtree", - "d3-sankey-circular", - "d3-scale", - "d3-selection", - "d3-shape", - "d3-time-format", - "d3-time", - "d3-timer", - "d3-transition", - "d3-voronoi", - "emotion", - "entities", - "escape-carriage", - "extend", - "fbjs", - "flat", - "inherits", - "is-alphabetical", - "is-alphanumerical", - "is-buffer", - "is-decimal", - "is-hexadecimal", - "is-plain-obj", - "is-whitespace-character", - "is-word-character", - "json2csv", - "labella", - "leaflet", - "lodash.clonedeep", - "lodash.curry", - "lodash.flatten", - "lodash.flow", - "lodash.get", - "lodash.set", - "lodash.uniq", - "lodash", - "martinez-polygon-clipping", - "markdown-escapes", - "material-colors", - "mdast-add-list-metadata", - "memoize-one", - "numeral", - "object-assign", - "os-browserify", - "parse-entities", - "path-browserify", - "polygon-offset", - "prismjs", - "process", - "prop-types", - "pure-color", - "react-annotation", - "react-base16-styling", - "react-color", - "react-dom", - "react-hot-loader", - "react-json-tree", - "react-markdown", - "react-table-hoc-fixed-columns", - "react-table", - "react", - "reactcss", - "remark-parse", - "repeat-string", - "roughjs-es5", - "schedule", - "semiotic-mark", - "semiotic", - "state-toggle", - "string-hash", - "style-loader", - "styled-jsx", - "stylis-rule-sheet", - "svg-inline-react", - "svg-path-bounding-box", - "svgpath", - "tinycolor2", - "tinyqueue", - "trim-trailing-lines", - "trim", - "trough", - "unherit", - "unified", - "uniqid", - "unist-util-is", - "unist-util-remove-position", - "unist-util-stringify-position", - "unist-util-visit-parents", - "unist-util-visit", - "vfile-location", - "vfile-message", - "vfile", - "viz-annotation", - "x-is-string", - "xtend" -] diff --git a/package.json b/package.json index 88b5cdf231d5..9f689b60ff34 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,33 @@ { "name": "python", "displayName": "Python", - "description": "Linting, Debugging (multi-threaded, remote), Intellisense, code formatting, refactoring, unit tests, snippets, and more.", - "version": "2019.1.0-alpha", - "languageServerVersion": "0.1.75", + "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 + }, + "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", + "enabledApiProposals": [ + "contribEditorContentMenu", + "quickPickSortByLabel", + "testObserver", + "quickPickItemTooltip", + "terminalDataWriteEvent", + "terminalExecuteCommandEvent", + "codeActionAI", + "notebookReplDocument", + "notebookVariableProvider" + ], "author": { "name": "Microsoft Corporation" }, @@ -17,32 +40,16 @@ "bugs": { "url": "https://github.com/Microsoft/vscode-python/issues" }, - "qna": "https://stackoverflow.com/questions/tagged/visual-studio-code+python", - "badges": [ - { - "url": "https://vscode-python.visualstudio.com/VSCode-Python/_apis/build/status/VSCode-Python-Rolling-CI?branchName=master", - "href": "https://vscode-python.visualstudio.com/VSCode-Python/VSCode-Python%20Team/_build/index?context=allDefinitions&path=&definitionId=9", - "description": "Continuous integration (VSTS)" - }, - { - "url": "https://travis-ci.org/Microsoft/vscode-python.svg?branch=master", - "href": "https://travis-ci.org/Microsoft/vscode-python", - "description": "Continuous integration (Travis)" - }, - { - "url": "https://codecov.io/gh/Microsoft/vscode-python/branch/master/graph/badge.svg", - "href": "https://codecov.io/gh/Microsoft/vscode-python", - "description": "Test coverage" - } - ], + "qna": "https://github.com/microsoft/vscode-python/discussions/categories/q-a", "icon": "icon.png", "galleryBanner": { "color": "#1e415e", "theme": "dark" }, "engines": { - "vscode": "^1.26.0" + "vscode": "^1.95.0" }, + "enableTelemetry": false, "keywords": [ "python", "django", @@ -52,1722 +59,1438 @@ "categories": [ "Programming Languages", "Debuggers", - "Linters", - "Snippets", - "Formatters", - "Other" + "Other", + "Data Science", + "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.viewTestOutput", - "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.updateSparkLibrary", - "onCommand:python.startREPL", - "onCommand:python.goToPythonObject", - "onCommand:python.setLinter", - "onCommand:python.enableLinting", - "onCommand:python.createTerminal", - "onCommand:python.discoverTests", - "onCommand:python.datascience.showhistorypane", - "onCommand:python.datascience.importnotebook", - "onCommand:python.datascience.selectjupyteruri", - "onCommand:python.datascience.exportfileasnotebook", - "onCommand:python.datascience.exportfileandoutputasnotebook", - "onCommand:python.python.enableSourceMapSupport" + "onCommand:python.copilotSetupTests", + "workspaceContains:mspythonconfig.json", + "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": [ + "walkthroughs": [ { - "command": "python.execSelectionInTerminal", - "key": "shift+enter", - "when": "editorFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed && !python.datascience.hascodecells" + "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" + } + } + ] }, { - "command": "python.datascience.runcurrentcelladvance", - "key": "shift+enter", - "when": "editorFocus && python.datascience.hascodecells && python.datascience.featureenabled" + "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%" + } + } + ] } ], - "commands": [ - { - "command": "python.enableSourceMapSupport", - "title": "%python.command.python.enableSourceMapSupport.title%", - "category": "Python" - }, + "breakpoints": [ { - "command": "python.sortImports", - "title": "%python.command.python.sortImports.title%", - "category": "Python Refactor" + "language": "html" }, { - "command": "python.startREPL", - "title": "%python.command.python.startREPL.title%", - "category": "Python" + "language": "jinja" }, { - "command": "python.createTerminal", - "title": "%python.command.python.createTerminal.title%", - "category": "Python" + "language": "python" }, { - "command": "python.buildWorkspaceSymbols", - "title": "%python.command.python.buildWorkspaceSymbols.title%", - "category": "Python" + "language": "django-html" }, { - "command": "python.runtests", - "title": "%python.command.python.runtests.title%", - "category": "Python" - }, + "language": "django-txt" + } + ], + "commands": [ { - "command": "python.debugtests", - "title": "%python.command.python.debugtests.title%", - "category": "Python" + "title": "%python.command.python.createNewFile.title%", + "shortTitle": "%python.menu.createNewFile.title%", + "category": "Python", + "command": "python.createNewFile" }, { - "command": "python.execInTerminal", - "title": "%python.command.python.execInTerminal.title%", - "category": "Python" + "category": "Python", + "command": "python.copyTestId", + "title": "%python.command.python.testing.copyTestId.title%" }, { - "command": "python.setInterpreter", - "title": "%python.command.python.setInterpreter.title%", - "category": "Python" + "category": "Python", + "command": "python.analysis.restartLanguageServer", + "title": "%python.command.python.analysis.restartLanguageServer.title%" }, { - "command": "python.updateSparkLibrary", - "title": "%python.command.python.updateSparkLibrary.title%", - "category": "Python" + "category": "Python", + "command": "python.clearCacheAndReload", + "title": "%python.command.python.clearCacheAndReload.title%" }, { - "command": "python.refactorExtractVariable", - "title": "%python.command.python.refactorExtractVariable.title%", - "category": "Python Refactor" + "category": "Python", + "command": "python.clearWorkspaceInterpreter", + "title": "%python.command.python.clearWorkspaceInterpreter.title%" }, { - "command": "python.refactorExtractMethod", - "title": "%python.command.python.refactorExtractMethod.title%", - "category": "Python Refactor" + "category": "Python", + "command": "python.configureTests", + "title": "%python.command.python.configureTests.title%" }, { - "command": "python.viewTestOutput", - "title": "%python.command.python.viewTestOutput.title%", - "category": "Python" + "category": "Python", + "command": "python.createTerminal", + "title": "%python.command.python.createTerminal.title%" }, { - "command": "python.selectAndRunTestMethod", - "title": "%python.command.python.selectAndRunTestMethod.title%", - "category": "Python" + "category": "Python", + "command": "python.createEnvironment", + "title": "%python.command.python.createEnvironment.title%" }, { - "command": "python.selectAndDebugTestMethod", - "title": "%python.command.python.selectAndDebugTestMethod.title%", - "category": "Python" + "category": "Python", + "command": "python.createEnvironment-button", + "title": "%python.command.python.createEnvironment.title%" }, { - "command": "python.selectAndRunTestFile", - "title": "%python.command.python.selectAndRunTestFile.title%", - "category": "Python" + "category": "Python", + "command": "python.execInTerminal", + "title": "%python.command.python.execInTerminal.title%" }, { - "command": "python.runCurrentTestFile", - "title": "%python.command.python.runCurrentTestFile.title%", - "category": "Python" + "category": "Python", + "command": "python.execInTerminal-icon", + "icon": "$(play)", + "title": "%python.command.python.execInTerminalIcon.title%" }, { - "command": "python.runFailedTests", - "title": "%python.command.python.runFailedTests.title%", - "category": "Python" + "category": "Python", + "command": "python.execInDedicatedTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInDedicatedTerminal.title%" }, { - "command": "python.discoverTests", - "title": "%python.command.python.discoverTests.title%", - "category": "Python" + "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.runcurrentcelladvance", - "title": "%python.command.python.datascience.runcurrentcelladvance.title%", - "category": "Python" - }, - { - "command": "python.datascience.showhistorypane", - "title": "%python.command.python.datascience.showhistorypane.title%", - "category": "Python" - }, - { - "command": "python.datascience.runallcells", - "title": "%python.command.python.datascience.runallcells.command.title%", - "category": "Python" - }, - { - "command": "python.datascience.runcell", - "title": "%python.command.python.datascience.runcell.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.datascience.importnotebook", - "title": "%python.command.python.datascience.importnotebook.title%", - "category": "Python" - }, - { - "command": "python.datascience.exportoutputasnotebook", - "title": "%python.command.python.datascience.exportoutputasnotebook.title%", - "category": "Python" - }, - { - "command": "python.datascience.exportfileasnotebook", - "title": "%python.command.python.datascience.exportfileasnotebook.title%", - "category": "Python" + "command": "python.execInREPL", + "title": "%python.command.python.execInREPL.title%" }, { - "command": "python.datascience.exportfileandoutputasnotebook", - "title": "%python.command.python.datascience.exportfileandoutputasnotebook.title%", - "category": "Python" + "category": "Python", + "command": "python.reportIssue", + "title": "%python.command.python.reportIssue.title%" }, { - "command": "python.datascience.undocells", - "title": "%python.command.python.datascience.undocells.title%", - "category": "Python" + "category": "Test", + "command": "testing.reRunFailTests", + "icon": "$(run-errors)", + "title": "%python.command.testing.rerunFailedTests.title%" }, { - "command": "python.datascience.redocells", - "title": "%python.command.python.datascience.redocells.title%", - "category": "Python" + "category": "Python", + "command": "python.setInterpreter", + "title": "%python.command.python.setInterpreter.title%" }, { - "command": "python.datascience.removeallcells", - "title": "%python.command.python.datascience.removeallcells.title%", - "category": "Python" + "category": "Python", + "command": "python.startREPL", + "title": "%python.command.python.startTerminalREPL.title%" }, { - "command": "python.datascience.interruptkernel", - "title": "%python.command.python.datascience.interruptkernel.title%", - "category": "Python" + "category": "Python", + "command": "python.startNativeREPL", + "title": "%python.command.python.startNativeREPL.title%" }, { - "command": "python.datascience.restartkernel", - "title": "%python.command.python.datascience.restartkernel.title%", - "category": "Python" + "category": "Python", + "command": "python.viewLanguageServerOutput", + "enablement": "python.hasLanguageServerOutputChannel", + "title": "%python.command.python.viewLanguageServerOutput.title%" }, { - "command": "python.datascience.expandallcells", - "title": "%python.command.python.datascience.expandallcells.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.collapseallcells", - "title": "%python.command.python.datascience.collapseallcells.title%", - "category": "Python" + "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" + "configuration": { + "properties": { + "python.activeStateToolPath": { + "default": "state", + "description": "%python.activeStateToolPath.description%", + "scope": "machine-overridable", + "type": "string" }, - { - "command": "python.refactorExtractMethod", - "title": "Refactor: Extract Method", - "group": "Refactor", - "when": "editorHasSelection && editorLangId == python" + "python.autoComplete.extraPaths": { + "default": [], + "description": "%python.autoComplete.extraPaths.description%", + "scope": "resource", + "type": "array", + "uniqueItems": true }, - { - "command": "python.sortImports", - "title": "Refactor: Sort Imports", - "group": "Refactor", - "when": "editorLangId == python" + "python.createEnvironment.contentButton": { + "default": "hide", + "markdownDescription": "%python.createEnvironment.contentButton.description%", + "scope": "machine-overridable", + "type": "string", + "enum": [ + "show", + "hide" + ] }, - { - "command": "python.execSelectionInTerminal", - "group": "Python", - "when": "editorFocus && editorLangId == python" + "python.createEnvironment.trigger": { + "default": "prompt", + "markdownDescription": "%python.createEnvironment.trigger.description%", + "scope": "machine-overridable", + "type": "string", + "enum": [ + "off", + "prompt" + ] }, - { - "command": "python.execSelectionInDjangoShell", - "group": "Python", - "when": "editorHasSelection && editorLangId == python && python.isDjangoProject" + "python.condaPath": { + "default": "", + "description": "%python.condaPath.description%", + "scope": "machine", + "type": "string" }, - { - "when": "resourceLangId == python", - "command": "python.execInTerminal", - "group": "Python" + "python.defaultInterpreterPath": { + "default": "python", + "markdownDescription": "%python.defaultInterpreterPath.description%", + "scope": "machine-overridable", + "type": "string" }, - { - "when": "resourceLangId == python", - "command": "python.runCurrentTestFile", - "group": "Python" + "python.envFile": { + "default": "${workspaceFolder}/.env", + "description": "%python.envFile.description%", + "scope": "resource", + "type": "string" }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled", - "command": "python.datascience.runcurrentcell", - "group": "Python" + "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", - "command": "python.datascience.runcurrentcelladvance", - "group": "Python" + "python.experiments.enabled": { + "default": true, + "description": "%python.experiments.enabled.description%", + "scope": "window", + "type": "boolean" }, - { - "when": "editorFocus && editorLangId == python && resourceLangId == jupyter && python.datascience.featureenabled", - "command": "python.datascience.importnotebook", - "group": "Python" + "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 }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled", - "command": "python.datascience.exportfileasnotebook", - "group": "Python2" + "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 }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled", - "command": "python.datascience.exportfileandoutputasnotebook", - "group": "Python2@2" - } - ], - "explorer/context": [ - { - "when": "resourceLangId == python", - "command": "python.runtests", - "group": "Python" + "python.globalModuleInstallation": { + "default": false, + "description": "%python.globalModuleInstallation.description%", + "scope": "resource", + "type": "boolean" }, - { - "when": "resourceLangId == python", - "command": "python.debugtests", - "group": "Python" + "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": "resourceLangId == python", - "command": "python.execInTerminal", - "group": "Python" + "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": "resourceLangId == jupyter", - "command": "python.datascience.importnotebook", - "group": "Python" - } - ], - "commandPalette": [ - { - "command": "python.datascience.runcurrentcell", - "title": "%python.command.python.datascience.runcurrentcell.title%", - "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled" + "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" }, - { - "command": "python.datascience.runcurrentcelladvance", - "title": "%python.command.python.datascience.runcurrentcelladvance.title%", - "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled" + "python.missingPackage.severity": { + "default": "Hint", + "description": "%python.missingPackage.severity.description%", + "enum": [ + "Error", + "Hint", + "Information", + "Warning" + ], + "scope": "resource", + "type": "string" }, - { - "command": "python.datascience.showhistorypane", - "title": "%python.command.python.datascience.showhistorypane.title%", - "category": "Python", - "when": "python.datascience.featureenabled" + "python.locator": { + "default": "js", + "description": "%python.locator.description%", + "enum": [ + "js", + "native" + ], + "tags": [ + "onExP", + "preview" + ], + "scope": "machine", + "type": "string" + }, + "python.pipenvPath": { + "default": "pipenv", + "description": "%python.pipenvPath.description%", + "scope": "machine-overridable", + "type": "string" + }, + "python.poetryPath": { + "default": "poetry", + "description": "%python.poetryPath.description%", + "scope": "machine-overridable", + "type": "string" + }, + "python.pixiToolPath": { + "default": "pixi", + "description": "%python.pixiToolPath.description%", + "scope": "machine-overridable", + "type": "string" + }, + "python.terminal.activateEnvInCurrentTerminal": { + "default": false, + "description": "%python.terminal.activateEnvInCurrentTerminal.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.datascience.runallcells", - "title": "%python.command.python.datascience.runallcells.command.title%", - "category": "Python", - "when": "python.datascience.featureenabled" + "python.terminal.activateEnvironment": { + "default": true, + "description": "%python.terminal.activateEnvironment.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.datascience.runcell", - "title": "%python.command.python.datascience.runcell.title%", - "category": "Python", - "when": "python.datascience.featureenabled" + "python.terminal.executeInFileDir": { + "default": false, + "description": "%python.terminal.executeInFileDir.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.datascience.importnotebook", - "title": "%python.command.python.datascience.importnotebook.title%", - "category": "Python" + "python.terminal.focusAfterLaunch": { + "default": false, + "description": "%python.terminal.focusAfterLaunch.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.datascience.exportfileasnotebook", - "title": "%python.command.python.datascience.exportfilesasnotebook.title%", - "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled" + "python.terminal.launchArgs": { + "default": [], + "description": "%python.terminal.launchArgs.description%", + "scope": "resource", + "type": "array" }, - { - "command": "python.datascience.exportfileandoutputasnotebook", - "title": "%python.command.python.datascience.exportfileandoutputasnotebook.title%", - "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled" + "python.terminal.shellIntegration.enabled": { + "default": true, + "markdownDescription": "%python.terminal.shellIntegration.enabled.description%", + "scope": "resource", + "type": "boolean", + "tags": [ + "preview" + ] }, - { - "command": "python.datascience.undocells", - "title": "%python.command.python.datascience.undocells.title%", - "category": "Python", - "when": "python.datascience.haveinteractivecells && python.datascience.featureenabled" + "python.REPL.enableREPLSmartSend": { + "default": true, + "description": "%python.EnableREPLSmartSend.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.datascience.redocells", - "title": "%python.command.python.datascience.redocells.title%", - "category": "Python", - "when": "python.datascience.haveredoablecells && python.datascience.featureenabled" + "python.REPL.sendToNativeREPL": { + "default": false, + "description": "%python.REPL.sendToNativeREPL.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.datascience.removeallcells", - "title": "%python.command.python.datascience.removeallcells.title%", - "category": "Python", - "when": "python.datascience.haveinteractivecells && python.datascience.featureenabled" + "python.REPL.provideVariables": { + "default": true, + "description": "%python.REPL.provideVariables.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.datascience.interruptkernel", - "title": "%python.command.python.datascience.interruptkernel.title%", - "category": "Python", - "when": "python.datascience.haveinteractive && python.datascience.featureenabled" + "python.testing.autoTestDiscoverOnSaveEnabled": { + "default": true, + "description": "%python.testing.autoTestDiscoverOnSaveEnabled.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.datascience.restartkernel", - "title": "%python.command.python.datascience.restartkernel.title%", - "category": "Python", - "when": "python.datascience.haveinteractive && python.datascience.featureenabled" + "python.testing.autoTestDiscoverOnSavePattern": { + "default": "**/*.py", + "description": "%python.testing.autoTestDiscoverOnSavePattern.description%", + "scope": "resource", + "type": "string" }, - { - "command": "python.datascience.expandallcells", - "title": "%python.command.python.datascience.expandallcells.title%", - "category": "Python", - "when": "python.datascience.haveinteractive && python.datascience.featureenabled" + "python.testing.cwd": { + "default": null, + "description": "%python.testing.cwd.description%", + "scope": "resource", + "type": "string" }, - { - "command": "python.datascience.collapseallcells", - "title": "%python.command.python.datascience.collapseallcells.title%", - "category": "Python", - "when": "python.datascience.haveinteractive && python.datascience.featureenabled" + "python.testing.debugPort": { + "default": 3000, + "description": "%python.testing.debugPort.description%", + "scope": "resource", + "type": "number" }, - { - "command": "python.datascience.exportoutputasnotebook", - "title": "%python.command.python.datascience.exportoutputasnotebook.title%", - "category": "Python", - "when": "python.datascience.haveinteractive && python.datascience.featureenabled" - } - ] - }, - "debuggers": [ - { - "type": "python", - "label": "Python", - "languages": [ - "python" - ], - "enableBreakpointsFor": { - "languageIds": [ - "python", - "html", - "jinja" - ] + "python.testing.promptToConfigure": { + "default": true, + "description": "%python.testing.promptToConfigure.description%", + "scope": "resource", + "type": "boolean" }, - "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", - "program": "./out/client/debugger/debugAdapter/main.js", - "runtime": "node", - "configurationSnippets": [ - { - "label": "Python: Terminal (integrated)", - "description": "%python.snippet.launch.terminal.description%", - "body": { - "name": "Python: Terminal (integrated)", - "type": "python", - "request": "launch", - "program": "^\"\\${file}\"", - "console": "integratedTerminal" - } - }, - { - "label": "Python: Terminal (external)", - "description": "%python.snippet.launch.externalTerminal.description%", - "body": { - "name": "Python: Terminal (external)", - "type": "python", - "request": "launch", - "program": "^\"\\${file}\"", - "console": "externalTerminal" - } + "python.testing.pytestArgs": { + "default": [], + "description": "%python.testing.pytestArgs.description%", + "items": { + "type": "string" }, - { - "label": "Python: Module", - "description": "%python.snippet.launch.module.description%", - "body": { - "name": "Python: Module", - "type": "python", - "request": "launch", - "module": "enter-your-module-name-here", - "console": "integratedTerminal" - } + "scope": "resource", + "type": "array" + }, + "python.testing.pytestEnabled": { + "default": false, + "description": "%python.testing.pytestEnabled.description%", + "scope": "resource", + "type": "boolean" + }, + "python.testing.pytestPath": { + "default": "pytest", + "description": "%python.testing.pytestPath.description%", + "scope": "machine-overridable", + "type": "string" + }, + "python.testing.unittestArgs": { + "default": [ + "-v", + "-s", + ".", + "-p", + "*test*.py" + ], + "description": "%python.testing.unittestArgs.description%", + "items": { + "type": "string" }, - { - "label": "Python: Django", - "description": "%python.snippet.launch.django.description%", - "body": { - "name": "Django", - "type": "python", - "request": "launch", - "program": "^\"\\${workspaceFolder}/manage.py\"", - "args": [ - "runserver", - "--noreload", - "--nothreading" - ], - "django": true - } + "scope": "resource", + "type": "array" + }, + "python.testing.unittestEnabled": { + "default": false, + "description": "%python.testing.unittestEnabled.description%", + "scope": "resource", + "type": "boolean" + }, + "python.venvFolders": { + "default": [], + "description": "%python.venvFolders.description%", + "items": { + "type": "string" }, - { - "label": "Python: Flask", - "description": "%python.snippet.launch.flask.description%", - "body": { - "name": "Flask", - "type": "python", - "request": "launch", - "module": "flask", - "env": { - "FLASK_APP": "app.py", - "FLASK_ENV": "development", - "FLASK_DEBUG": "0" + "scope": "machine", + "type": "array", + "uniqueItems": true + }, + "python.venvPath": { + "default": "", + "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" }, - "args": [ - "run", - "--no-debugger", - "--no-reload" - ], - "jinja": true - } - }, - { - "label": "Python: Gevent", - "description": "%python.snippet.launch.gevent.description%", - "body": { - "name": "Gevent", - "type": "python", - "request": "launch", - "program": "^\"\\${file}\"", - "gevent": true - } - }, - { - "label": "Python: PySpark", - "description": "%python.snippet.launch.pyspark.description%", - "body": { - "name": "PySpark", - "type": "python", - "request": "launch", - "osx": { - "pythonPath": "^\"\\${env:SPARK_HOME}/bin/spark-submit\"" + "debugAdapterPath": { + "description": "Path (fully qualified) to the python debug adapter executable.", + "type": "string" }, - "windows": { - "pythonPath": "^\"\\${env:SPARK_HOME}/bin/spark-submit.cmd\"" + "django": { + "default": false, + "description": "Django debugging.", + "type": "boolean" }, - "linux": { - "pythonPath": "^\"\\${env:SPARK_HOME}/bin/spark-submit\"" + "host": { + "default": "127.0.0.1", + "description": "Hostname or IP address to connect to.", + "type": "string" }, - "program": "^\"\\${file}\"" - } - }, - { - "label": "Python: Watson", - "description": "%python.snippet.launch.watson.description%", - "body": { - "name": "Watson", - "type": "python", - "request": "launch", - "program": "^\"\\${workspaceFolder}/console.py\"", - "args": [ - "dev", - "runserver", - "--noreload=True" - ], - "jinja": true - } - }, - { - "label": "Python: Scrapy", - "description": "%python.snippet.launch.scrapy.description%", - "body": { - "name": "Scrapy", - "type": "python", - "request": "launch", - "module": "scrapy", - "args": [ - "crawl", - "specs", - "-o", - "bikes.json" - ] - } - }, - { - "label": "Python: Pyramid", - "description": "%python.snippet.launch.pyramid.description%", - "body": { - "name": "Pyramid", - "type": "python", - "request": "launch", - "args": [ - "^\"\\${workspaceFolder}/development.ini\"" - ], - "pyramid": true, - "jinja": true - } - }, - { - "label": "Python: Attach", - "description": "%python.snippet.launch.attach.description%", - "body": { - "name": "Attach (Remote Debug)", - "type": "python", - "request": "attach", - "port": 5678, - "host": "localhost" - } - } - ], - "configurationAttributes": { - "launch": { - "properties": { - "module": { - "type": "string", - "description": "Name of the module to be debugged.", - "default": "" + "jinja": { + "default": null, + "description": "Jinja template debugging (e.g. Flask).", + "enum": [ + false, + null, + true + ] }, - "program": { - "type": "string", - "description": "Absolute path to the program.", - "default": "${file}" + "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" }, - "pythonPath": { - "type": "string", - "description": "Path (fully qualified) to python executable. Defaults to the value in settings.json", - "default": "${config:python.pythonPath}" + "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" }, - "args": { - "type": "array", - "description": "Command line arguments passed to the program", + "logToFile": { + "default": false, + "description": "Enable logging of debugger events to a log file.", + "type": "boolean" + }, + "pathMappings": { "default": [], "items": { - "type": "string" - } + "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" }, - "stopOnEntry": { - "type": "boolean", - "description": "Automatically stop after launch.", - "default": false + "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": { - "type": "boolean", + "default": true, "description": "Show return value of functions when stepping.", - "default": false + "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": [ - "none", + "externalTerminal", "integratedTerminal", - "externalTerminal" - ], - "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.", - "default": "integratedTerminal" + "internalConsole" + ] + }, + "consoleTitle": { + "default": "Python Debug Console", + "description": "Display name of the debug console or terminal" }, "cwd": { - "type": "string", + "default": "${workspaceFolder}", "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).", - "default": "${workspaceFolder}" + "type": "string" + }, + "debugAdapterPath": { + "description": "Path (fully qualified) to the python debug adapter executable.", + "type": "string" + }, + "django": { + "default": false, + "description": "Django debugging.", + "type": "boolean" }, "env": { - "type": "object", + "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.", - "default": {} + "type": "object" }, "envFile": { - "type": "string", + "default": "${workspaceFolder}/.env", "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" - }, - "logToFile": { - "type": "boolean", - "description": "Enable logging of debugger events to a log file.", - "default": false - }, - "redirectOutput": { - "type": "boolean", - "description": "Redirect output.", - "default": true - }, - "debugStdLib": { - "type": "boolean", - "description": "Debug standard library code.", - "default": false + "type": "string" }, "gevent": { - "type": "boolean", + "default": false, "description": "Enable debugging of gevent monkey-patched code.", - "default": false + "type": "boolean" }, - "django": { - "type": "boolean", - "description": "Django debugging.", - "default": false + "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": [ - 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 + null, + true + ] }, - "pyramid": { - "type": "boolean", - "description": "Whether debugging Pyramid applications", - "default": false + "justMyCode": { + "default": true, + "description": "Debug only user-written code.", + "type": "boolean" }, - "subProcess": { - "type": "boolean", - "description": "Whether to enable Sub Process debugging", - "default": false - } - } - }, - "attach": { - "required": [ - "port" - ], - "properties": { - "port": { - "type": "number", - "description": "Debug port to attach", - "default": 0 + "logToFile": { + "default": false, + "description": "Enable logging of debugger events to a log file.", + "type": "boolean" }, - "host": { - "type": "string", - "description": "IP Address of the of remote server (default is localhost or use 127.0.0.1).", - "default": "localhost" + "module": { + "default": "", + "description": "Name of the module to be debugged.", + "type": "string" }, "pathMappings": { - "type": "array", - "label": "Path mappings.", + "default": [], "items": { - "type": "object", "label": "Path mapping", - "required": [ - "localRoot", - "remoteRoot" - ], "properties": { "localRoot": { - "type": "string", + "default": "${workspaceFolder}", "label": "Local source root.", - "default": "${workspaceFolder}" + "type": "string" }, "remoteRoot": { - "type": "string", + "default": "", "label": "Remote source root.", - "default": "" + "type": "string" } - } + }, + "required": [ + "localRoot", + "remoteRoot" + ], + "type": "object" }, - "default": [] + "label": "Path mappings.", + "type": "array" }, - "logToFile": { - "type": "boolean", - "description": "Enable logging of debugger events to a log file.", - "default": false + "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": { - "type": "boolean", + "default": true, "description": "Redirect output.", - "default": true - }, - "debugStdLib": { - "type": "boolean", - "description": "Debug standard library code.", - "default": false + "type": "boolean" }, - "django": { - "type": "boolean", - "description": "Django debugging.", - "default": false + "showReturnValue": { + "default": true, + "description": "Show return value of functions when stepping.", + "type": "boolean" }, - "jinja": { - "enum": [ - true, - false, - null - ], - "description": "Jinja template debugging (e.g. Flask).", - "default": null + "stopOnEntry": { + "default": false, + "description": "Automatically stop after launch.", + "type": "boolean" }, "subProcess": { - "type": "boolean", + "default": false, "description": "Whether to enable Sub Process debugging", - "default": false + "type": "boolean" + }, + "sudo": { + "default": false, + "description": "Running debug program under elevated permissions (on Unix).", + "type": "boolean" } } } - } - } - ], - "configuration": { - "type": "object", - "title": "Python Configuration", - "properties": { - "python.diagnostics.sourceMapsEnabled": { - "type": "boolean", - "default": false, - "description": "Enable source map support for meaningful strack 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.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.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.jupyterServerURI": { - "type": "string", - "default": "local", - "description": "Select the Jupyter server URI to connect to. Select 'local' to launch a new Juypter server on the local machine.", - "scope": "resource" - }, - "python.dataScience.notebookFileRoot": { - "type": "string", - "default": "${workspaceFolder}", - "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": true, - "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.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.jediEnabled": { - "type": "boolean", - "default": true, - "description": "Enables Jedi as IntelliSense engine instead of Microsoft Python Analysis Engine.", - "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).", - "scope": "resource" - }, - "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.typeshedPaths": { - "type": "array", - "default": [], - "items": { - "type": "string" - }, - "description": "Paths to look for typeshed modules.", - "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.logLevel": { - "type": "string", - "enum": [ - "Error", - "Warning", - "Information", - "Trace" - ], - "default": "Error", - "description": "Defines type of log messages language server writes into the output window.", - "scope": "resource" - }, - "python.analysis.symbolsHierarchyDepthLimit": { - "type": "integer", - "default": 10, - "description": "Limits depth of the symbol tree in the document outline.", - "scope": "resource" - }, - "python.linting.enabled": { - "type": "boolean", - "default": true, - "description": "Whether to lint Python files.", - "scope": "resource" - }, - "python.linting.flake8Args": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], - "items": { - "type": "string" - }, - "scope": "resource" - }, - "python.linting.flake8CategorySeverity.E": { - "type": "string", - "default": "Error", - "description": "Severity of Flake8 message type 'E'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], - "scope": "resource" - }, - "python.linting.flake8CategorySeverity.F": { - "type": "string", - "default": "Error", - "description": "Severity of Flake8 message type 'F'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], - "scope": "resource" - }, - "python.linting.flake8CategorySeverity.W": { - "type": "string", - "default": "Warning", - "description": "Severity of Flake8 message type 'W'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], - "scope": "resource" - }, - "python.linting.flake8Enabled": { - "type": "boolean", - "default": false, - "description": "Whether to lint Python files using flake8", - "scope": "resource" - }, - "python.linting.flake8Path": { - "type": "string", - "default": "flake8", - "description": "Path to flake8, you can use a custom version of flake8 by modifying this setting to include the full path.", - "scope": "resource" - }, - "python.linting.ignorePatterns": { - "type": "array", - "description": "Patterns used to exclude files or folders from being linted.", - "default": [ - ".vscode/*.py", - "**/site-packages/**/*.py" - ], - "items": { - "type": "string" - }, - "scope": "resource" - }, - "python.linting.lintOnSave": { - "type": "boolean", - "default": true, - "description": "Whether to lint Python files when saved.", - "scope": "resource" - }, - "python.linting.maxNumberOfProblems": { - "type": "number", - "default": 100, - "description": "Controls the maximum number of problems produced by the server.", - "scope": "resource" - }, - "python.linting.banditArgs": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], - "items": { - "type": "string" - }, - "scope": "resource" - }, - "python.linting.banditEnabled": { - "type": "boolean", - "default": false, - "description": "Whether to lint Python files using bandit.", - "scope": "resource" - }, - "python.linting.banditPath": { - "type": "string", - "default": "bandit", - "description": "Path to bandit, you can use a custom version of bandit by modifying this setting to include the full path.", - "scope": "resource" - }, - "python.linting.mypyArgs": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [ - "--ignore-missing-imports", - "--follow-imports=silent", - "--show-column-numbers" - ], - "items": { - "type": "string" - }, - "scope": "resource" - }, - "python.linting.mypyCategorySeverity.error": { - "type": "string", - "default": "Error", - "description": "Severity of Mypy message type 'Error'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], - "scope": "resource" - }, - "python.linting.mypyCategorySeverity.note": { - "type": "string", - "default": "Information", - "description": "Severity of Mypy message type 'Note'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], - "scope": "resource" - }, - "python.linting.mypyEnabled": { - "type": "boolean", - "default": false, - "description": "Whether to lint Python files using mypy.", - "scope": "resource" - }, - "python.linting.mypyPath": { - "type": "string", - "default": "mypy", - "description": "Path to mypy, you can use a custom version of mypy by modifying this setting to include the full path.", - "scope": "resource" - }, - "python.linting.pep8Args": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], - "items": { - "type": "string" - }, - "scope": "resource" - }, - "python.linting.pep8CategorySeverity.E": { - "type": "string", - "default": "Error", - "description": "Severity of Pep8 message type 'E'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], - "scope": "resource" - }, - "python.linting.pep8CategorySeverity.W": { - "type": "string", - "default": "Warning", - "description": "Severity of Pep8 message type 'W'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], - "scope": "resource" - }, - "python.linting.pep8Enabled": { - "type": "boolean", - "default": false, - "description": "Whether to lint Python files using pep8", - "scope": "resource" - }, - "python.linting.pep8Path": { - "type": "string", - "default": "pep8", - "description": "Path to pep8, you can use a custom version of pep8 by modifying this setting to include the full path.", - "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": [], - "items": { - "type": "string" - }, - "scope": "resource" - }, - "python.linting.pylamaEnabled": { - "type": "boolean", - "default": false, - "description": "Whether to lint Python files using pylama.", - "scope": "resource" - }, - "python.linting.pylamaPath": { - "type": "string", - "default": "pylama", - "description": "Path to pylama, you can use a custom version of pylama by modifying this setting to include the full path.", - "scope": "resource" - }, - "python.linting.pylintArgs": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], - "items": { - "type": "string" - }, - "scope": "resource" - }, - "python.linting.pylintCategorySeverity.convention": { - "type": "string", - "default": "Information", - "description": "Severity of Pylint message type 'Convention/C'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], - "scope": "resource" - }, - "python.linting.pylintCategorySeverity.error": { - "type": "string", - "default": "Error", - "description": "Severity of Pylint message type 'Error/E'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], - "scope": "resource" - }, - "python.linting.pylintCategorySeverity.fatal": { - "type": "string", - "default": "Error", - "description": "Severity of Pylint message type 'Fatal/F'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], - "scope": "resource" - }, - "python.linting.pylintCategorySeverity.refactor": { - "type": "string", - "default": "Hint", - "description": "Severity of Pylint message type 'Refactor/R'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], - "scope": "resource" - }, - "python.linting.pylintCategorySeverity.warning": { - "type": "string", - "default": "Warning", - "description": "Severity of Pylint message type 'Warning/W'.", - "enum": [ - "Hint", - "Error", - "Information", - "Warning" - ], - "scope": "resource" - }, - "python.linting.pylintEnabled": { - "type": "boolean", - "default": true, - "description": "Whether to lint Python files using pylint.", - "scope": "resource" - }, - "python.linting.pylintPath": { - "type": "string", - "default": "pylint", - "description": "Path to Pylint, you can use a custom version of pylint by modifying this setting to include the full path.", - "scope": "resource" - }, - "python.linting.pylintUseMinimalCheckers": { - "type": "boolean", - "default": true, - "description": "Whether to run Pylint with minimal set of rules.", - "scope": "resource" + "deprecated": "%python.debugger.deprecatedMessage%", + "configurationSnippets": [], + "label": "Python", + "languages": [ + "python" + ], + "type": "python", + "variables": { + "pickProcess": "python.pickLocalProcess" }, - "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" + "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.condaPath": { - "type": "string", - "default": "", - "description": "Path to the conda executable to use for activation (version 4.4+).", - "scope": "resource" + { + "category": "Python", + "command": "python.clearCacheAndReload", + "title": "%python.command.python.clearCacheAndReload.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.sortImports.args": { - "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.sortImports.path": { - "type": "string", - "description": "Path to isort script, default using inner version", - "default": "", - "scope": "resource" + { + "category": "Python", + "command": "python.configureTests", + "title": "%python.command.python.configureTests.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.terminal.activateEnvironment": { - "type": "boolean", - "default": true, - "description": "Activate Python Environment in Terminal created using the Extension.", - "scope": "resource" + { + "category": "Python", + "command": "python.createEnvironment", + "title": "%python.command.python.createEnvironment.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "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" + { + "category": "Python", + "command": "python.createEnvironment-button", + "title": "%python.command.python.createEnvironment.title%", + "when": "false" }, - "python.terminal.launchArgs": { - "type": "array", - "default": [], - "description": "Python launch arguments to use when executing a file in the terminal.", - "scope": "resource" + { + "category": "Python", + "command": "python.createTerminal", + "title": "%python.command.python.createTerminal.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.unitTest.cwd": { - "type": "string", - "default": null, - "description": "Optional working directory for unit tests.", - "scope": "resource" + { + "category": "Python", + "command": "python.execInTerminal", + "title": "%python.command.python.execInTerminal.title%", + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, - "python.unitTest.debugPort": { - "type": "number", - "default": 3000, - "description": "Port number used for debugging of unittests.", - "scope": "resource" + { + "category": "Python", + "command": "python.execInTerminal-icon", + "icon": "$(play)", + "title": "%python.command.python.execInTerminalIcon.title%", + "when": "false" }, - "python.unitTest.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.execInDedicatedTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInDedicatedTerminal.title%", + "when": "false" }, - "python.unitTest.nosetestsEnabled": { - "type": "boolean", - "default": false, - "description": "Whether to enable or disable unit testing using nosetests.", - "scope": "resource" + { + "category": "Python", + "command": "python.execSelectionInDjangoShell", + "title": "%python.command.python.execSelectionInDjangoShell.title%", + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, - "python.unitTest.nosetestPath": { - "type": "string", - "default": "nosetests", - "description": "Path to nosetests, you can use a custom version of nosetests by modifying this setting to include the full path.", - "scope": "resource" + { + "category": "Python", + "command": "python.execSelectionInTerminal", + "title": "%python.command.python.execSelectionInTerminal.title%", + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, - "python.unitTest.promptToConfigure": { - "type": "boolean", - "default": true, - "description": "Where to prompt to configure a test framework if potential tests directories are discovered.", - "scope": "resource" + { + "category": "Python", + "command": "python.copyTestId", + "title": "%python.command.python.testing.copyTestId.title%", + "when": "false" }, - "python.unitTest.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.execInREPL", + "title": "%python.command.python.execInREPL.title%", + "when": "false" }, - "python.unitTest.pyTestEnabled": { - "type": "boolean", - "default": false, - "description": "Whether to enable or disable unit testing using pytest.", - "scope": "resource" + { + "category": "Python", + "command": "python.reportIssue", + "title": "%python.command.python.reportIssue.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.unitTest.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": "Test", + "command": "testing.reRunFailTests", + "icon": "$(run-errors)", + "title": "%python.command.testing.rerunFailedTests.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.unitTest.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.setInterpreter", + "title": "%python.command.python.setInterpreter.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.unitTest.unittestEnabled": { - "type": "boolean", - "default": false, - "description": "Whether to enable or disable unit testing using unittest.", - "scope": "resource" + { + "category": "Python", + "command": "python.startREPL", + "title": "%python.command.python.startTerminalREPL.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.unitTest.autoTestDiscoverOnSaveEnabled": { - "type": "boolean", - "default": true, - "description": "Whether to enable or disable auto run test discovery when saving a unit test file.", - "scope": "resource" + { + "category": "Python", + "command": "python.startNativeREPL", + "title": "%python.command.python.startNativeREPL.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.venvFolders": { - "type": "array", - "default": [ - "envs", - ".pyenv", - ".direnv" - ], - "description": "Folders in your home directory to look into for virtual environments.", - "scope": "resource", - "items": { - "type": "string" - } + { + "category": "Python", + "command": "python.viewLanguageServerOutput", + "enablement": "python.hasLanguageServerOutputChannel", + "title": "%python.command.python.viewLanguageServerOutput.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "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.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.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" + { + "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.workspaceSymbols.enabled": { - "type": "boolean", - "default": true, - "description": "Set to 'false' to disable Workspace Symbol provider using ctags.", - "scope": "resource" + { + "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.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" + { + "command": "python.execSelectionInDjangoShell", + "group": "Python", + "when": "editorHasSelection && editorLangId == python && python.isDjangoProject && !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" + { + "command": "python.execSelectionInTerminal", + "group": "Python", + "when": "!config.python.REPL.sendToNativeREPL && editorFocus && editorLangId == python && !virtualWorkspace && shellExecutionSupported" }, - "python.workspaceSymbols.rebuildOnStart": { - "type": "boolean", - "default": true, - "description": "Whether to re-build the tags file on start (defaults to true).", - "scope": "resource" + { + "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.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" + { + "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": [ - "Pipfile" - ] - }, - { - "id": "json", - "filenames": [ - "Pipfile.lock" - ] - }, + "submenus": [ { - "id": "jinja", - "extensions": [ - ".jinja2", - ".j2" - ], - "aliases": [ - "Jinja" - ] + "id": "python.run", + "label": "%python.editor.context.submenu.runPython%", + "icon": "$(play)" }, { - "id": "jupyter", - "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" @@ -1781,219 +1504,310 @@ "url": "./schemas/conda-meta.json" } ], - "yamlValidation": [ + "languageModelTools": [ { - "fileMatch": ".condarc", - "url": "./schemas/condarc.json" + "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 <env_name> -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" + "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" + ] + } + }, + { + "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" + ], + "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", + "package": "gulp clean && gulp prePublishBundle && vsce package -o ms-python-insiders.vsix", + "prePublish": "gulp clean && gulp prePublishNonBundle", "compile": "tsc -watch -p ./", - "compile-webviews-watch": "npx webpack --config webpack.datascience-ui.config.js --watch", - "dump-datascience-webpack-stats": "webpack --config webpack.datascience-ui.config.js --profile --json > tmp/ds-stats.json", - "compile-webviews": "gulp compile-webviews", - "compile-webviews-verbose": "npx webpack --config webpack.datascience-ui.config.js", - "postinstall": "node ./node_modules/vscode/bin/install", + "compileApi": "node ./node_modules/typescript/lib/tsc.js -b ./pythonExtensionApi/tsconfig.json", + "compiled": "deemon npm run compile", + "kill-compiled": "deemon --kill npm run compile", + "checkDependencies": "gulp checkDependencies", "test": "node ./out/test/standardTest.js && node ./out/test/multiRootTest.js", - "test:unittests": "mocha --require source-map-support/register --opts ./build/.mocha.unittests.opts", - "test:unittests:cover": "nyc --nycrc-path ./build/.nycrc npm run test:unittests", - "test:functional": "mocha --require source-map-support/register --opts ./build/.mocha.functional.opts", - "test:functional:cover": "nyc --nycrc-path ./build/.nycrc npm run test:functional", - "testDebugger": "node ./out/test/debuggerTest.js", - "testSingleWorkspace": "node ./out/test/standardTest.js", - "testMultiWorkspace": "node ./out/test/multiRootTest.js", - "testPerformance": "node ./out/test/performanceTest.js", - "testSmoke": "node ./out/test/smokeTest.js", + "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": "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", + "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": "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", + "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", - "cover:enable": "gulp cover:enable", - "debugger-coverage": "gulp debugger-coverage", - "cover:inlinesource": "gulp inlinesource" + "addExtensionPackDependencies": "gulp addExtensionPackDependencies", + "updateBuildNumber": "gulp updateBuildNumber", + "verifyBundle": "gulp verifyBundle", + "webpack": "webpack" }, "dependencies": { - "@jupyterlab/services": "^3.1.4", + "@iarna/toml": "^3.0.0", + "@vscode/extension-telemetry": "^0.8.4", "arch": "^2.1.0", - "azure-storage": "^2.10.1", - "diff-match-patch": "^1.0.0", - "dotenv": "^5.0.1", - "file-matcher": "^1.3.0", - "fs-extra": "^4.0.3", - "fuzzy": "^0.1.3", - "get-port": "^3.2.0", - "glob": "^7.1.2", - "iconv-lite": "^0.4.21", - "inversify": "^4.11.1", - "line-by-line": "^0.1.6", - "lodash": "^4.17.11", - "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-stream-zip": "^1.6.0", - "opn": "^5.3.0", - "pidusage": "^1.2.0", - "reflect-metadata": "^0.1.12", - "request": "^2.87.0", - "request-progress": "^3.0.0", - "rxjs": "^5.5.9", - "semver": "^5.5.0", - "sudo-prompt": "^8.2.0", - "tmp": "^0.0.29", - "tree-kill": "^1.2.0", - "typescript-char": "^0.0.0", - "uint64be": "^1.0.1", - "unicode": "^10.0.0", - "untildify": "^3.0.2", - "vscode-debugadapter": "^1.28.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-extension-telemetry": "^0.1.0", - "vscode-languageclient": "^4.4.0", - "vscode-languageserver": "^4.4.0", - "vscode-languageserver-protocol": "^3.10.3", + "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.4.19" + "xml2js": "^0.5.0" }, "devDependencies": { - "@babel/core": "^7.1.0", - "@babel/preset-env": "^7.1.0", - "@babel/preset-react": "^7.0.0", - "@nteract/transform-dataresource": "^4.3.5", - "@nteract/transform-geojson": "^3.2.3", - "@nteract/transform-model-debug": "^3.2.3", - "@nteract/transform-plotly": "^3.2.3", - "@nteract/transforms": "^4.4.4", + "@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/del": "^3.0.0", - "@types/dotenv": "^4.0.3", - "@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/istanbul": "^0.4.29", - "@types/jsdom": "^11.12.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/mocha": "^2.2.48", - "@types/node": "9.4.7", - "@types/prismjs": "^1.9.0", - "@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/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": "^4.3.0", - "@types/temp": "^0.8.32", - "@types/tmp": "0.0.33", - "@types/untildify": "^3.0.0", - "@types/uuid": "^3.4.3", - "@types/webpack-bundle-analyzer": "^2.13.0", + "@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", - "JSONStream": "^1.3.2", - "ansi-to-html": "^0.6.7", - "awesome-typescript-loader": "^5.2.1", - "babel-loader": "^8.0.3", - "babel-plugin-inline-json-import": "^0.3.1", - "babel-plugin-prismjs": "^1.0.2", - "babel-plugin-transform-runtime": "^6.23.0", - "babel-polyfill": "^6.26.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", - "codecov": "^3.0.0", - "colors": "^1.2.1", - "copy-webpack-plugin": "^4.6.0", + "copy-webpack-plugin": "^9.1.0", + "cross-env": "^7.0.3", "cross-spawn": "^6.0.5", - "css-loader": "^1.0.1", - "decache": "^4.4.0", - "del": "^3.0.0", - "download": "^7.0.0", - "enzyme": "^3.7.0", - "enzyme-adapter-react-16": "^1.6.0", - "event-stream": "3.3.4", - "file-loader": "^2.0.0", - "flat": "^4.0.0", - "gulp": "^4.0.0", - "gulp-debounced-watch": "^1.0.4", - "gulp-filter": "^5.1.0", - "gulp-inline-source": "^3.2.0", - "gulp-json-editor": "^2.2.2", - "gulp-sourcemaps": "^2.6.4", - "gulp-typescript": "^4.0.1", - "gulp-watch": "^5.0.0", - "html-webpack-plugin": "^3.2.0", - "husky": "^1.1.2", - "is-running": "^2.1.0", - "istanbul": "^0.4.5", - "jsdom": "^12.2.0", - "json-loader": "^0.5.7", - "loader-utils": "^1.1.0", - "mocha": "^5.0.4", - "mocha-junit-reporter": "^1.17.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-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", - "nyc": "^13.1.0", - "prismjs": "^1.15.0", - "raw-loader": "^0.5.1", - "react": "^16.5.2", - "react-dev-utils": "^5.0.2", - "react-dom": "^16.5.2", - "react-json-tree": "^0.11.0", - "relative": "^3.0.2", - "remap-istanbul": "^0.10.1", - "retyped-diff-match-patch-tsd-ambient": "^1.0.0-0", + "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", - "source-map-support": "^0.5.9", - "style-loader": "^0.23.1", - "styled-jsx": "^3.1.0", - "svg-inline-loader": "^0.8.0", - "svg-inline-react": "^3.1.0", - "ts-loader": "^5.3.0", - "ts-mockito": "^2.3.1", + "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", - "tslint": "^5.9.1", - "tslint-eslint-rules": "^5.1.0", - "tslint-microsoft-contrib": "^5.0.3", - "typed-react-markdown": "^0.1.0", "typemoq": "^2.1.0", - "typescript": "^3.2.2", - "typescript-formatter": "^7.1.0", - "url-loader": "^1.1.1", - "uuid": "^3.3.2", - "vscode": "^1.1.22", - "vscode-debugadapter-testsupport": "^1.27.0", - "webpack": "^4.20.2", - "webpack-bundle-analyzer": "^3.0.3", - "webpack-cli": "^3.1.2", + "typescript": "~5.2", + "uuid": "^14.0.0", + "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", - "wrapper-webpack-plugin": "^2.0.0", - "yargs": "^12.0.2" - }, - "__metadata": { - "id": "f1f59ae4-9318-4f3c-a9b5-81b2eaa5f8a5", - "publisherDisplayName": "Microsoft", - "publisherId": "998b010b-e2af-44a5-a6cd-0b5fd3b9b6f8" + "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" } } diff --git a/package.nls.de.json b/package.nls.de.json deleted file mode 100644 index 4c6af269cd11..000000000000 --- a/package.nls.de.json +++ /dev/null @@ -1,52 +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.updateSparkLibrary.title": "PySpark Arbeitsplatz-Bibliotheken aktualisieren", - "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.standard.description": "Python-Programm debuggen mit Standardausgabe", - "python.snippet.launch.pyspark.label": "Python: PySpark", - "python.snippet.launch.pyspark.description": "PySpark debuggen", - "python.snippet.launch.module.label": "Python: Modul", - "python.snippet.launch.module.description": "Python-Modul debuggen", - "python.snippet.launch.terminal.label": "Python: Terminal (integriert)", - "python.snippet.launch.terminal.description": "Python-Programm mit integriertem Terminal/Konsole debuggen", - "python.snippet.launch.externalTerminal.label": "Python: Terminal (extern)", - "python.snippet.launch.externalTerminal.description": "Python-Programm mit externem Terminal/Konsole debuggen", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.django.description": "Django-Anwendung debuggen", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.flask.description": "Flask-Anwendung debuggen", - "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x oder früher)", - "python.snippet.launch.flaskOld.description": "Ältere Flask-Anwendung debuggen", - "python.snippet.launch.gevent.label": "Python: Gevent", - "python.snippet.launch.gevent.description": "Gevent-Anwendung debuggen", - "python.snippet.launch.pyramid.label": "Python: Pyramid-Anwendung", - "python.snippet.launch.pyramid.description": "Pyramid-Anwendung debuggen", - "python.snippet.launch.watson.label": "Python: Watson-Anwendung", - "python.snippet.launch.watson.description": "Watson-Anwendung debuggen", - "python.snippet.launch.attach.label": "Python: Anfügen", - "python.snippet.launch.attach.description": "Debugger anfügen für Remote Debugging", - "python.snippet.launch.scrapy.label": "Python: Scrapy", - "python.snippet.launch.scrapy.description": "Scrapy mit integiertem Terminal/Konsole" -} diff --git a/package.nls.es.json b/package.nls.es.json deleted file mode 100644 index 9160b00c73ee..000000000000 --- a/package.nls.es.json +++ /dev/null @@ -1,52 +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.updateSparkLibrary.title": "Actualizar las librerías PySpark del area de trabajo", - "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.standard.description": "Depurar una aplicación Python con salida estándar", - "python.snippet.launch.pyspark.label": "Python: PySpark", - "python.snippet.launch.pyspark.description": "Depurar una aplicación de PySpark", - "python.snippet.launch.module.label": "Python: Módulo", - "python.snippet.launch.module.description": "Depurar un módulo de Python", - "python.snippet.launch.terminal.label": "Python: Terminal (integrada)", - "python.snippet.launch.terminal.description": "Depurar una aplicación Python usando la terminal integrada", - "python.snippet.launch.externalTerminal.label": "Python: Terminal (externa)", - "python.snippet.launch.externalTerminal.description": "Depurar una aplicación Python usando una terminal externa", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.django.description": "Depurar una aplicación de Django", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.flask.description": "Depurar una aplicación de Flask", - "python.snippet.launch.flaskOld.label": "Python: Flask (Versión 0.10.x o anterior)", - "python.snippet.launch.flaskOld.description": "Depurar una aplicación de Flask de estilo antiguo", - "python.snippet.launch.gevent.label": "Python: Gevent", - "python.snippet.launch.gevent.description": "Depurar una aplicación de Gevent", - "python.snippet.launch.pyramid.label": "Python: Pyramid", - "python.snippet.launch.pyramid.description": "Depurar una aplicación de Pyramid", - "python.snippet.launch.watson.label": "Python: Watson", - "python.snippet.launch.watson.description": "Depurar una aplicación de Watson", - "python.snippet.launch.attach.label": "Python: Adjuntar", - "python.snippet.launch.attach.description": "Depuración remota usando depurador adjunto", - "python.snippet.launch.scrapy.label": "Python: Scrapy", - "python.snippet.launch.scrapy.description": "Scrapy usando la terminal integrada" -} diff --git a/package.nls.fr.json b/package.nls.fr.json deleted file mode 100644 index 196c4ac8db06..000000000000 --- a/package.nls.fr.json +++ /dev/null @@ -1,49 +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.updateSparkLibrary.title": "Mettre à jour les librairies de l'espace de travail PySpark", - "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.standard.description": "Déboguer un programme Python avec la sortie standard", - "python.snippet.launch.pyspark.label": "Python : PySpark", - "python.snippet.launch.pyspark.description": "Déboguer PySpark", - "python.snippet.launch.module.label": "Python: Module", - "python.snippet.launch.module.description": "Déboguer un module Python", - "python.snippet.launch.terminal.label": "Python : Terminal (intégré)", - "python.snippet.launch.terminal.description": "Déboguer un programme Python avec la console intégrée", - "python.snippet.launch.externalTerminal.label": "Python : Terminal (externe)", - "python.snippet.launch.externalTerminal.description": "Déboguer un programme Python avec une console externe", - "python.snippet.launch.django.label": "Python : Django", - "python.snippet.launch.django.description": "Déboguer une application Django", - "python.snippet.launch.flask.label": "Python : Flask", - "python.snippet.launch.flask.description": "Déboguer une application Flask", - "python.snippet.launch.flaskOld.label": "Python : Flask (0.10.x ou antérieur)", - "python.snippet.launch.flaskOld.description": "Déboguer une application Flask (0.10.x ou antérieur)", - "python.snippet.launch.pyramid.label": "Python : application Pyramid", - "python.snippet.launch.pyramid.description": "Déboguer une application Pyramid", - "python.snippet.launch.watson.label": "Python: Application Watson", - "python.snippet.launch.watson.description": "Déboguer une Application Watson", - "python.snippet.launch.attach.label": "Python: Attacher", - "python.snippet.launch.attach.description": "Attacher le débogueur pour un débogage distant", - "python.snippet.launch.scrapy.label": "Python : Scrapy", - "python.snippet.launch.scrapy.description": "Scrapy avec un terminal intégré" -} diff --git a/package.nls.it.json b/package.nls.it.json deleted file mode 100644 index 1643731c81f3..000000000000 --- a/package.nls.it.json +++ /dev/null @@ -1,50 +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.updateSparkLibrary.title": "Aggiorna librerie PySpark dello spazio di lavoro", - "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.standard.description": "Esegui debug di un programma Python su output predefinito", - "python.snippet.launch.pyspark.label": "Python: PySpark", - "python.snippet.launch.pyspark.description": "Esegui debug PySpark", - "python.snippet.launch.module.label": "Python: Modulo", - "python.snippet.launch.module.description": "Esegui debug modulo Python", - "python.snippet.launch.terminal.label": "Python: Terminale (integrato)", - "python.snippet.launch.terminal.description": "Esegui debug di un programma Python nel terminale integrato", - "python.snippet.launch.externalTerminal.label": "Python: Terminale (esterno)", - "python.snippet.launch.externalTerminal.description": "Esegui debug di un programma Python nel terminale esterno", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.django.description": "Esegui debug applicazione Django", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.flask.description": "Esegui debug applicazione Flask", - "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x o precedente)", - "python.snippet.launch.flaskOld.description": "Esegui debug applicazione Flask in vecchio stile", - "python.snippet.launch.pyramid.label": "Python: Applicazione Pyramid", - "python.snippet.launch.pyramid.description": "Esegui debug applicazione Pyramid", - "python.snippet.launch.watson.label": "Python: Applicazione Watson", - "python.snippet.launch.watson.description": "Esegui debug applicazione Watson", - "python.snippet.launch.attach.label": "Python: Allega", - "python.snippet.launch.attach.description": "Allega debugger per debug remoto", - "python.snippet.launch.scrapy.label": "Python: Scrapy", - "python.snippet.launch.scrapy.description": "Scrapy con terminale integrato", - "LanguageService.bannerLabelYes": "Sì, prenderò il sondaggio ora" -} diff --git a/package.nls.ja.json b/package.nls.ja.json deleted file mode 100644 index 7768ca8d097f..000000000000 --- a/package.nls.ja.json +++ /dev/null @@ -1,45 +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.updateSparkLibrary.title": "ワークスペース PySpark ライブラリを更新", - "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.standard.description": "標準出力で Python プログラムをデバッグ", - "python.snippet.launch.pyspark.label": "Python: PySpark", - "python.snippet.launch.pyspark.description": "PySpark をデバッグ", - "python.snippet.launch.module.label": "Python: モジュール", - "python.snippet.launch.module.description": "Python モジュールをデバッグ", - "python.snippet.launch.terminal.label": "Python: ターミナル (統合)", - "python.snippet.launch.terminal.description": "統合ターミナル/コンソールで Python プログラムをデバッグ", - "python.snippet.launch.externalTerminal.label": "Python: ターミナル (外部)", - "python.snippet.launch.externalTerminal.description": "外部のターミナル/コンソールで Python プログラムをデバッグ", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.django.description": "Django アプリケーションをデバッグ", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.flask.description": "Flask アプリケーションをデバッグ", - "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x 以前)", - "python.snippet.launch.flaskOld.description": "旧式の Flask アプリケーションをデバッグ", - "python.snippet.launch.pyramid.label": "Python: Pyramid アプリケーション", - "python.snippet.launch.pyramid.description": "Pyramid アプリケーションをデバッグ", - "python.snippet.launch.watson.label": "Python: Watson アプリケーション", - "python.snippet.launch.watson.description": "Watson アプリケーションをデバッグ", - "python.snippet.launch.attach.label": "Python: アタッチ", - "python.snippet.launch.attach.description": "リモートデバッグのためにデバッガをアタッチ", - "python.snippet.launch.scrapy.label": "Python: Scrapy", - "python.snippet.launch.scrapy.description": "統合ターミナル/コンソールで Scrapy を実行" -} diff --git a/package.nls.json b/package.nls.json index 767d76f4e4e7..57f2ed95b2c0 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,181 +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 Unit Tests", - "python.command.python.debugtests.title": "Debug All Unit 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.updateSparkLibrary.title": "Update Workspace PySpark Libraries", - "python.command.python.refactorExtractVariable.title": "Extract Variable", - "python.command.python.refactorExtractMethod.title": "Extract Method", - "python.command.python.viewTestOutput.title": "Show Unit Test Output", - "python.command.python.selectAndRunTestMethod.title": "Run Unit Test Method ...", - "python.command.python.selectAndDebugTestMethod.title": "Debug Unit Test Method ...", - "python.command.python.selectAndRunTestFile.title": "Run Unit Test File ...", - "python.command.python.runCurrentTestFile.title": "Run Current Unit Test File", - "python.command.python.runFailedTests.title": "Run Failed Unit Tests", - "python.command.python.discoverTests.title": "Discover Unit Tests", + "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", + "python.command.python.viewOutput.title": "Show Output", + "python.command.python.installJupyter.title": "Install the Jupyter extension", + "python.command.python.viewLanguageServerOutput.title": "Show Language Server Output", + "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.runallcells.command.title": "Run Current File in Python Interactive window", - "python.command.python.datascience.runallcells.title": "Run All Cells", - "python.command.python.datascience.runcurrentcell.title": "Run Current Cell", - "python.command.python.datascience.runcurrentcelladvance.title": "Run Current Cell And Advance", - "python.command.python.datascience.runcell.title": "Run Cell", - "python.command.python.datascience.showhistorypane.title": "Show Python Interactive window", - "python.command.python.datascience.selectjupyteruri.title": "Specify Jupyter server URI", - "python.command.python.datascience.importnotebook.title": "Import Jupyter Notebook", - "python.command.python.datascience.importnotebookonfile.title": "Import Jupyter Notebook", - "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.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.snippet.launch.standard.label": "Python: Current File", - "python.snippet.launch.standard.description": "Debug a Python Program with Standard Output", - "python.snippet.launch.pyspark.label": "Python: PySpark", - "python.snippet.launch.pyspark.description": "Debug PySpark", - "python.snippet.launch.module.label": "Python: Module", - "python.snippet.launch.module.description": "Debug a Python Module", - "python.snippet.launch.terminal.label": "Python: Terminal (integrated)", - "python.snippet.launch.terminal.description": "Debug a Python program with Integrated Terminal/Console", - "python.snippet.launch.externalTerminal.label": "Python: Terminal (external)", - "python.snippet.launch.externalTerminal.description": "Debug a Python program with External Terminal/Console", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.django.description": "Debug a Django Application", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.flask.description": "Debug a Flask Application", - "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x or earlier)", - "python.snippet.launch.flaskOld.description": "Debug an older styled Flask Application", - "python.snippet.launch.gevent.label": "Python: Gevent", - "python.snippet.launch.gevent.description": "Debug a Gevent Application", - "python.snippet.launch.pyramid.label": "Python: Pyramid Application", - "python.snippet.launch.pyramid.description": "Debug a Pyramid Application", - "python.snippet.launch.watson.label": "Python: Watson Application", - "python.snippet.launch.watson.description": "Debug a Watson Application", - "python.snippet.launch.attach.label": "Python: Attach", - "python.snippet.launch.attach.description": "Attach the Debugger for Remote Debugging", - "python.snippet.launch.scrapy.label": "Python: Scrapy", - "python.snippet.launch.scrapy.description": "Scrapy with Integrated Terminal/Console", - "LanguageService.bannerMessage": "Can you please take 2 minutes to tell us how the Python Language Server is working for you?", - "LanguageService.bannerLabelYes": "Yes, take survey now", - "LanguageService.bannerLabelNo": "No, thanks", - "DataScience.unknownMimeTypeFormat": "Mime type {0} is not currently supported.", - "DataScience.historyTitle": "Python Interactive", - "DataScience.badWebPanelFormatString": "<html><body><h1>{0} is not a valid file name</h1></body></html>", - "DataScience.sessionDisposed": "Cannot execute code, session has been disposed.", - "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.collapseInputTooltip": "Collapse input block", - "DataScience.importDialogTitle": "Import Jupyter Notebook", - "DataScience.importDialogFilter": "Jupyter Notebooks", - "DataScience.notebookCheckForImportYes": "Import", - "DataScience.notebookCheckForImportNo": "Later", - "DataScience.notebookCheckForImportDontAskAgain": "Don't Ask Again", - "DataScience.notebookCheckForImportTitle": "Do you want to import the Jupyter Notebook into Python code?", - "DataScience.jupyterNotSupported": "Running cells requires Jupyter notebooks to be installed.", - "DataScience.jupyterNbConvertNotSupported": "Importing notebooks requires Jupyter nbconvert to be installed.", - "DataScience.jupyterLaunchTimedOut": "The Jupyter notebook server failed to launch in time", - "DataScience.jupyterLaunchNoURL": "Failed to find the URL of the launched Jupyter notebook server", - "DataScience.pythonInteractiveHelpLink": "Get more help", - "DataScience.importingFormat": "Importing {0}", - "DataScience.startingJupyter": "Starting Jupyter server", - "DataScience.connectingToJupyter": "Connecting to Jupyter server", - "Interpreters.RefreshingInterpreters": "Refreshing Python Interpreters", - "Interpreters.LoadingInterpreters": "Loading Python Interpreters", - "DataScience.restartKernelMessage": "Do you want to restart the iPython kernel? All variables will be lost.", - "DataScience.restartKernelMessageYes": "Restart", - "DataScience.restartKernelMessageNo": "Cancel", - "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", - "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.pythonVersionHeaderNoPyKernel": "Python version may not match, no ipykernel found:", - "DataScience.pythonRestartHeader": "Restarted Kernel:", - "Linter.InstalledButNotEnabled": "Linter {0} is installed but not enabled.", - "Linter.replaceWithSelectedLinter": "Multiple linters are enabled in settings. Replace with '{0}'?", - "DataScience.jupyterSelectURILaunchLocal": "Launch a local Jupyter server when needed", - "DataScience.jupyterSelectURISpecifyURI": "Type in the URI to connect to a running Jupyter server", - "DataScience.jupyterSelectURIPrompt": "Enter the URI of a Jupyter server", - "DataScience.jupyterSelectURIInvalidURI": "Invalid URI specified", - "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.notebookVersionFormat": "Jupyter Notebook Version: {0}", - "DataScience.jupyterKernelNotSupportedOnActive": "Jupyter kernel cannot be started from '{0}'. Using closest match {1} instead.", - "DataScience.jupyterKernelSpecNotFound": "Cannot create a Jupyter kernel spec and none are available for use", - "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 Language Server. Reverting to the alternative, Jedi.", - "DataScience.interruptKernel": "Interrupt iPython Kernel", - "DataScience.exportingFormat": "Exporting {0}", - "DataScience.exportCancel": "Cancel", - "Common.canceled": "Canceled", - "DataScience.importChangeDirectoryComment": "#%% Change working directory from the workspace root to the ipynb file location. Turn this addition off with the DataSciece.changeDirOnImportExport setting", - "DataScience.exportChangeDirectoryComment": "# Change directory to VSCode workspace root so that relative path loads work correctly. Turn this addition off with the DataSciece.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: ", - "Common.loadingPythonExtension": "Python extension loading...", - "debug.selectConfigurationTitle": "Select a debug configuration", - "debug.selectConfigurationPlaceholder": "Debug Configuration", - "debug.debugFileConfigurationLabel": "Python File", - "debug.debugFileConfigurationDescription": "Debug Python file", - "debug.debugModuleConfigurationLabel": "Module", - "debug.debugModuleConfigurationDescription": "Debug Python module/package", - "debug.remoteAttachConfigurationLabel": "Remote Attach", - "debug.remoteAttachConfigurationDescription": "Debug a remote Python program", - "debug.debugDjangoConfigurationLabel": "Django", - "debug.debugDjangoConfigurationDescription": "Web Application", - "debug.debugFlaskConfigurationLabel": "Flask", - "debug.debugFlaskConfigurationDescription": "Web Application", - "debug.debugPyramidConfigurationLabel": "Pyramid", - "debug.debugPyramidConfigurationDescription": "Web Application", - "debug.djangoEnterManagePyPathTitle": "Debug Django", - "debug.djangoEnterManagePyPathPrompt": "Enter path to manage.py ('${workspaceFolderToken}' points to the root of the current workspace folder)", - "debug.djangoEnterManagePyPathInvalidFilePathError": "Enter a valid Python file path", - "debug.flaskEnterAppPathOrNamePathTitle": "Debug Flask", - "debug.flaskEnterAppPathOrNamePathPrompt": "Enter path to application, e.g. 'app.py' or 'app'", - "debug.flaskEnterAppPathOrNamePathInvalidNameError": "Enter a valid name", - "debug.moduleEnterModuleTitle": "Debug Module", - "debug.moduleEnterModulePrompt": "Enter Python module/package name", - "debug.moduleEnterModuleInvalidNameError": "Enter a valid name", - "debug.pyramidEnterDevelopmentIniPathTitle": "Debug Pyramid", - "debug.pyramidEnterDevelopmentIniPathPrompt": "`Enter path to development.ini ('${workspaceFolderToken}' points to the root of the current workspace folder)`", - "debug.pyramidEnterDevelopmentIniPathInvalidFilePathError": "Enter a valid file path", - "debug.attachRemotePortTitle": "Remote Debugging", - "debug.attachRemotePortPrompt": "Enter port number", - "debug.attachRemotePortValidationError": "Enter a valid port number", - "debug.attachRemoteHostTitle": "Remote Debugging", - "debug.attachRemoteHostPrompt": "Enter a host name or IP address", - "debug.attachRemoteHostValidationError": "Enter a valid host name or IP address", - "UnitTests.testErrorDiagnosticMessage": "Error", - "UnitTests.testFailDiagnosticMessage": "Fail", - "UnitTests.testSkippedDiagnosticMessage": "Skipped" + "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.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 5534ed4bef06..000000000000 --- a/package.nls.ko-kr.json +++ /dev/null @@ -1,45 +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.updateSparkLibrary.title": "PySpark 작업 영역 라이브러리 업데이트", - "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.standard.description": "표준 출력으로 Python 프로그램 디버그", - "python.snippet.launch.pyspark.label": "Python: PySpark", - "python.snippet.launch.pyspark.description": "PySpark 디버그", - "python.snippet.launch.module.label": "Python: 모듈", - "python.snippet.launch.module.description": "Python 모듈 디버그", - "python.snippet.launch.terminal.label": "Python: 터미널 (통합)", - "python.snippet.launch.terminal.description": "통합 터미널/콘솔에서 Python 프로그램 디버그", - "python.snippet.launch.externalTerminal.label": "Python: 터미널 (외부)", - "python.snippet.launch.externalTerminal.description": "외부 터미널/콘솔에서 Python 프로그램 디버그", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.django.description": "Django 응용 프로그램 디버그", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.flask.description": "Flask 응용 프로그램 디버그", - "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x 또는 이전 버전)", - "python.snippet.launch.flaskOld.description": "이전 스타일의 Flask 응용 프로그램 디버그", - "python.snippet.launch.pyramid.label": "Python: Pyramid 응용 프로그램", - "python.snippet.launch.pyramid.description": "Pyramid 응용 프로그램 디버그", - "python.snippet.launch.watson.label": "Python: Watson 응용 프로그램", - "python.snippet.launch.watson.description": "Watson 응용 프로그램 디버그", - "python.snippet.launch.attach.label": "Python: 연결", - "python.snippet.launch.attach.description": "원격 디버깅을 위해 디버거에 연결", - "python.snippet.launch.scrapy.label": "Python: Scrapy", - "python.snippet.launch.scrapy.description": "통합 터미널/콘솔에서 Scrapy 실행" -} diff --git a/package.nls.pt-br.json b/package.nls.pt-br.json deleted file mode 100644 index 5b9505e81c9b..000000000000 --- a/package.nls.pt-br.json +++ /dev/null @@ -1,52 +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.updateSparkLibrary.title": "Atualizar Área de Trabalho da Biblioteca PySpark", - "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.standard.description": "Depurar um Programa Python com a saída padrão", - "python.snippet.launch.pyspark.label": "Python: PySpark", - "python.snippet.launch.pyspark.description": "Depurar PySpark", - "python.snippet.launch.module.label": "Python: Módulo", - "python.snippet.launch.module.description": "Depurar um Módulo Python", - "python.snippet.launch.terminal.label": "Python: Terminal (integrado)", - "python.snippet.launch.terminal.description": "Depurar um Programa Python com Terminal/Console Integrado", - "python.snippet.launch.externalTerminal.label": "Python: Terminal (externo)", - "python.snippet.launch.externalTerminal.description": "Depurar um Programa Python com Terminal/Console Externo", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.django.description": "Depurar uma Aplicação Django", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.flask.description": "Depurar uma Aplicação Flask", - "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x ou inferior)", - "python.snippet.launch.flaskOld.description": "Depurar uma Aplicação Flask no Estilo Antigo", - "python.snippet.launch.gevent.label": "Python: Gevent", - "python.snippet.launch.gevent.description": "Depurar uma Aplicação Gevent", - "python.snippet.launch.pyramid.label": "Python: Aplicação Pyramid", - "python.snippet.launch.pyramid.description": "Depurar uma Aplicação Pyramid", - "python.snippet.launch.watson.label": "Python: Aplicação Watson", - "python.snippet.launch.watson.description": "Depurar uma Aplicação Watson", - "python.snippet.launch.attach.label": "Python: Anexar", - "python.snippet.launch.attach.description": "Anexar depurador para depuração remota", - "python.snippet.launch.scrapy.label": "Python: Scrapy", - "python.snippet.launch.scrapy.description": "Scrapy com Terminal/Console Integrado" -} diff --git a/package.nls.ru.json b/package.nls.ru.json deleted file mode 100644 index 31247aa54535..000000000000 --- a/package.nls.ru.json +++ /dev/null @@ -1,54 +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.updateSparkLibrary.title": "Обновить библиотеки PySpark", - "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.standard.description": "Отладить программу Python со стандартным выводом", - "python.snippet.launch.pyspark.label": "Python: PySpark", - "python.snippet.launch.pyspark.description": "Отладка PySpark", - "python.snippet.launch.module.label": "Python: Модуль", - "python.snippet.launch.module.description": "Отладка модуля", - "python.snippet.launch.terminal.label": "Python: Интегрированная консоль", - "python.snippet.launch.terminal.description": "Отладка программы Python в интегрированной консоли", - "python.snippet.launch.externalTerminal.label": "Python: Внешний терминал", - "python.snippet.launch.externalTerminal.description": "Отладка программы Python во внешней консоли", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.django.description": "Отладка приложения Django", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.flask.description": "Отладка приложения Flask", - "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x или старее)", - "python.snippet.launch.flaskOld.description": "Отладка приложения Flask (старый стиль)", - "python.snippet.launch.gevent.label": "Python: Gevent", - "python.snippet.launch.gevent.description": "Отладка приложения Gevent", - "python.snippet.launch.pyramid.label": "Python: Приложение Pyramid", - "python.snippet.launch.pyramid.description": "Отладка приложения Pyramid", - "python.snippet.launch.watson.label": "Python: Приложение Watson", - "python.snippet.launch.watson.description": "Отладка приложения Watson", - "python.snippet.launch.attach.label": "Python: Подключить отладчик", - "python.snippet.launch.attach.description": "Подключить отладчик для удаленной отладки", - "python.snippet.launch.scrapy.label": "Python: Scrapy", - "python.snippet.launch.scrapy.description": "Scrapy в интегрированной консоли", - "LanguageService.bannerMessage": "Не могли бы вы уделить 2 минуты, чтобы рассказать нам насколько хорошо у вас работает Python Language Server?", - "LanguageService.bannerLabelYes": "Да, пройти опрос сейчас", - "LanguageService.bannerLabelNo": "Нет, спасибо" -} diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json deleted file mode 100644 index f7100b8ae56f..000000000000 --- a/package.nls.zh-cn.json +++ /dev/null @@ -1,52 +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.updateSparkLibrary.title": "更新工作区 PySpark 库", - "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.standard.description": "使用标准输出调试 Python 应用", - "python.snippet.launch.pyspark.label": "Python: PySpark", - "python.snippet.launch.pyspark.description": "调试 PySpark", - "python.snippet.launch.module.label": "Python: 模块", - "python.snippet.launch.module.description": "调试 Python 模块", - "python.snippet.launch.terminal.label": "Python: 终端 (集成)", - "python.snippet.launch.terminal.description": "使用集成终端调试 Python 程序", - "python.snippet.launch.externalTerminal.label": "Python: 终端 (外部)", - "python.snippet.launch.externalTerminal.description": "使用外部终端调试 Python 程序", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.django.description": "调试 Django 应用", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.flask.description": "调试 Flask 应用", - "python.snippet.launch.flaskOld.label": "Python: Flask (0.10.x 或之前)", - "python.snippet.launch.flaskOld.description": "调试旧式 Flask 应用", - "python.snippet.launch.gevent.label": "Python: Gevent 应用", - "python.snippet.launch.gevent.description": "调试 Gevent 应用", - "python.snippet.launch.pyramid.label": "Python: Pyramid 应用", - "python.snippet.launch.pyramid.description": "调试 Pyramid 应用", - "python.snippet.launch.watson.label": "Python: Watson 应用", - "python.snippet.launch.watson.description": "调试 Watson 应用", - "python.snippet.launch.attach.label": "Python: 附加", - "python.snippet.launch.attach.description": "附加远程调试器", - "python.snippet.launch.scrapy.label": "Python: Scrapy 应用", - "python.snippet.launch.scrapy.description": "使用集成终端运行 Scrapy" -} diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json deleted file mode 100644 index 388527016a2c..000000000000 --- a/package.nls.zh-tw.json +++ /dev/null @@ -1,52 +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.updateSparkLibrary.title": "更新工作區 PySpark 函式庫", - "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: Current File", - "python.snippet.launch.standard.description": "使用標準輸出偵錯 Python 程式", - "python.snippet.launch.pyspark.label": "Python:PySpark", - "python.snippet.launch.pyspark.description": "偵錯 PySpark", - "python.snippet.launch.module.label": "Python:模組", - "python.snippet.launch.module.description": "偵錯 Python 模組", - "python.snippet.launch.terminal.label": "Python:終端機(整合)", - "python.snippet.launch.terminal.description": "使用整合終端機偵錯 Python 程式", - "python.snippet.launch.externalTerminal.label": "Python:終端機(外部)", - "python.snippet.launch.externalTerminal.description": "使用外部終端機偵錯 Python 程式", - "python.snippet.launch.django.label": "Python:Django", - "python.snippet.launch.django.description": "偵錯 Django 程式", - "python.snippet.launch.flask.label": "Python:Flask", - "python.snippet.launch.flask.description": "偵錯 Flask 程式", - "python.snippet.launch.flaskOld.label": "Python:Flask(0.10.x 或之前)", - "python.snippet.launch.flaskOld.description": "偵錯舊式 Flask 程式", - "python.snippet.launch.pyramid.label": "Python:Pyramid 程式", - "python.snippet.launch.pyramid.description": "偵錯 Pyramid 程式", - "python.snippet.launch.watson.label": "Python:Watson 程式", - "python.snippet.launch.watson.description": "偵錯 Watson 程式", - "python.snippet.launch.attach.label": "Python:附加", - "python.snippet.launch.attach.description": "附加遠端偵錯工具", - "python.snippet.launch.scrapy.label": "Python:Scrapy 程式", - "python.snippet.launch.scrapy.description": "使用整合終端機執行 Scrapy", - "python.command.python.discoverTests.title": "探索 Unit 測試項目", - "python.snippet.launch.gevent.label": "Python: Gevent", - "python.snippet.launch.gevent.description": "偵錯 Gevent 應用程式" -} diff --git a/pvsc-dev-ext.py b/pvsc-dev-ext.py deleted file mode 100644 index 1eded83076f7..000000000000 --- a/pvsc-dev-ext.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -"""Setup and maintain a development build of PVSC. - -You must have git, node, and npm installed. - -""" -# Downloading the development build of the ``.vsix` was considered, but it actually -# takes longer to install due to the number of files to unzip compared to the -# incremental updates working from a git clone. - -import argparse -import enum -import os -import pathlib -import shutil -import subprocess -import sys - - -REPO_URL = "https://github.com/Microsoft/vscode-python.git" - - -@enum.unique -class VSCode(enum.Enum): - """Enum representing the install types of VS Code.""" - stable = ".vscode" - insiders = ".vscode-insiders" - - -def run_command(command, cwd=None): - """Run the specified command in a subprocess shell.""" - executable = shutil.which(command[0]) - command[0] = executable - cmd = subprocess.run(command, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) - cmd.check_returncode() - - -def checkout_directory(install_type, dir_name="vscode-python"): - return pathlib.Path.home() / install_type.value / "extensions" / dir_name - - -def clone_repo(clone_to, repo, branch): - """Clone the repository to the appropriate location.""" - # https://code.visualstudio.com/docs/editor/extension-gallery#_where-are-extensions-installed - cmd = ["git", "clone", "-q", "--single-branch", "--branch", branch, repo, os.fspath(clone_to)] - run_command(cmd) - - -def update_checkout(checkout): - """Update the code the latest version.""" - run_command(["git", "pull", "-q", "origin", "master"], cwd=checkout) - - -def install_npm_dependencies(checkout): - """Install packages from npm.""" - run_command(["npm", "--silent", "--no-progress", "install", "--no-save"], cwd=checkout) - - -def build_typescript(checkout): - """Compile all TypeScript code in the extension.""" - tsc_path = pathlib.Path("node_modules") / "typescript" / "bin" / "tsc" - run_command(["node", os.fspath(tsc_path), "-p", os.fspath(checkout)], cwd=checkout) - - -def install_PyPI_packages(checkout): - """Install packages from PyPI.""" - libs_path = checkout / "pythonFiles" / "lib" / "python" - requirements_path = checkout / "requirements.txt" - cmd = [ - sys.executable, - "-m", - "pip", - "-q", - "--disable-pip-version-check", - "install", - "--target", - os.fspath(libs_path), - "--no-cache-dir", - "--implementation", - "py", - "--no-deps", - "--upgrade", - "-r", - os.fspath(requirements_path), - ] - run_command(cmd) - - -def cleanup(checkout): - """Delete files downloaded by the extension.""" - for path in checkout.glob("languageServer*"): - if path.is_dir(): - shutil.rmtree(path) - - -def build(checkout): - """Install dependencies and build the extension.""" - print("Installing npm dependencies ...") - install_npm_dependencies(checkout) - print("Building TypeScript files ...") - build_typescript(checkout) - print("Installing PyPI packages ...") - install_PyPI_packages(checkout) - - -def setup(install_type, repo, branch): - """Set up a clone of PVSC.""" - checkout = checkout_directory(install_type) - print(f"Cloning {repo} ...") - clone_repo(checkout, repo, branch) - build(checkout) - - -def update(): - """Update development installs of PVSC.""" - for install_type in VSCode: - checkout = checkout_directory(install_type) - if not checkout.exists(): - continue - print(f"UPDATING {checkout}") - print("Deleting files downloaded by the extension ...") - cleanup(checkout) - print("Updating clone ...") - update_checkout(checkout) - build(checkout) - - -def parse_args(args=sys.argv[1:]): - """Parse CLI arguments.""" - parser = argparse.ArgumentParser(description="Setup and maintain a development build of PVSC (requires git, node, and npm)") - subparsers = parser.add_subparsers(dest="cmd") - setup_parser = subparsers.add_parser("setup") - setup_parser.add_argument("install_type", choices=[install_type.name for install_type in VSCode]) - setup_parser.add_argument('--repo', dest='repo', default=REPO_URL) - setup_parser.add_argument('--branch', dest='branch', default='master') - update_parser = subparsers.add_parser("update") - return parser.parse_args(args) - - -if __name__ == "__main__": - args = parse_args() - try: - if args.cmd == "setup": - setup(VSCode[args.install_type], args.repo, args.branch) - elif args.cmd == "update": - update() - else: - raise RuntimeError(f"Unrecognized sub-command: {args.cmd!r}") - except subprocess.CalledProcessError as exc: - print(f"Failed to run command {exc.cmd} : {exc.stderr}") 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 @@ +<!-- BEGIN MICROSOFT SECURITY.MD V0.0.5 BLOCK --> + +## 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). + +<!-- END MICROSOFT SECURITY.MD BLOCK --> 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<void>; + 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<string[]>; + + /** + * Gets the path to the debugger package used by the extension. + */ + getDebuggerPackagePath(): Promise<string | undefined>; + }; + + /** + * 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<void>; + /** + * This event is triggered when the active environment setting changes. + */ + readonly onDidChangeActiveEnvironmentPath: Event<ActiveEnvironmentPathChangeEvent>; + /** + * 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<EnvironmentsChangeEvent>; + /** + * 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<void>; + /** + * 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<ResolvedEnvironment | undefined>; + /** + * 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<EnvironmentVariablesChangeEvent>; + }; +} + +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<PythonExtension> { + 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/completion.py b/pythonFiles/completion.py deleted file mode 100644 index d0b8e4a0866e..000000000000 --- a/pythonFiles/completion.py +++ /dev/null @@ -1,652 +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.environment = jedi.api.environment.Environment(sys.prefix, sys.executable) - 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', ''), - sys_path=sys.path, environment=self.environment) - - 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/datascience/getServerInfo.py b/pythonFiles/datascience/getServerInfo.py deleted file mode 100644 index 5bf78d569513..000000000000 --- a/pythonFiles/datascience/getServerInfo.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from notebook.notebookapp import list_running_servers -import json - -server_list = list_running_servers() - -server_info_list = [] - -for si in server_list: - server_info_object = {} - server_info_object["base_url"] = si['base_url'] - server_info_object["notebook_dir"] = si['notebook_dir'] - server_info_object["hostname"] = si['hostname'] - server_info_object["password"] = si['password'] - server_info_object["pid"] = si['pid'] - server_info_object["port"] = si['port'] - server_info_object["secure"] = si['secure'] - server_info_object["token"] = si['token'] - server_info_object["url"] = si['url'] - server_info_list.append(server_info_object) - -print(json.dumps(server_info_list)) \ No newline at end of file diff --git a/pythonFiles/interpreterInfo.py b/pythonFiles/interpreterInfo.py deleted file mode 100644 index 4822594bd046..000000000000 --- a/pythonFiles/interpreterInfo.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json -import sys - -obj = {} -obj["versionInfo"] = sys.version_info[:4] -obj["sysPrefix"] = sys.prefix -obj["version"] = sys.version -obj["is64Bit"] = sys.maxsize > 2**32 - -print(json.dumps(obj)) diff --git a/pythonFiles/normalizeForInterpreter.py b/pythonFiles/normalizeForInterpreter.py deleted file mode 100644 index 8027f4db92c0..000000000000 --- a/pythonFiles/normalizeForInterpreter.py +++ /dev/null @@ -1,128 +0,0 @@ -import ast -import io -import operator -import os -import sys -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. - 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. - - """ - lines = 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 consequtive 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/ptvsd_launcher.py b/pythonFiles/ptvsd_launcher.py deleted file mode 100644 index 75aaca418fa9..000000000000 --- a/pythonFiles/ptvsd_launcher.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os -import os.path -import sys -import traceback - -useCustomPtvsd = sys.argv[1] == '--custom' -ptvsdArgs = sys.argv[:] -ptvsdArgs.pop(1) - -# Load the debugger package -try: - ptvs_lib_path = os.path.join(os.path.dirname(__file__), 'lib', 'python') - if useCustomPtvsd: - sys.path.append(ptvs_lib_path) - else: - sys.path.insert(0, ptvs_lib_path) - try: - import ptvsd - import ptvsd.debugger as vspd - from ptvsd.__main__ import main - ptvsd_loaded = True - except ImportError: - ptvsd_loaded = False - raise - vspd.DONT_DEBUG.append(os.path.normcase(__file__)) -except: - traceback.print_exc() - print(''' -Internal error detected. Please copy the above traceback and report at -https://github.com/Microsoft/vscode-python/issues/new - -Press Enter to close. . .''') - try: - raw_input() - except NameError: - input() - sys.exit(1) -finally: - if ptvs_lib_path: - sys.path.remove(ptvs_lib_path) - -main(ptvsdArgs) diff --git a/pythonFiles/refactor.py b/pythonFiles/refactor.py deleted file mode 100644 index d8def8a95df8..000000000000 --- a/pythonFiles/refactor.py +++ /dev/null @@ -1,303 +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 68f1126438db..000000000000 --- a/pythonFiles/sortImports.py +++ /dev/null @@ -1,12 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os -import os.path -import sys - -isort_path = os.path.join(os.path.dirname(__file__), 'lib', 'python') -sys.path.insert(0, isort_path) - -import isort.main -isort.main.main() diff --git a/pythonFiles/testlauncher.py b/pythonFiles/testlauncher.py deleted file mode 100644 index 5ce4532347f6..000000000000 --- a/pythonFiles/testlauncher.py +++ /dev/null @@ -1,64 +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 exclude_current_file_from_debugger(): - # Load the debugger package - try: - import ptvsd - import ptvsd.debugger as vspd - vspd.DONT_DEBUG.append(os.path.normcase(__file__)) - except: - traceback.print_exc() - print(''' -Internal error detected. Please copy the above traceback and report at -https://github.com/Microsoft/vscode-python/issues/new - -Press Enter to close. . .''') - try: - raw_input() - except NameError: - input() - sys.exit(1) - - -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__': - exclude_current_file_from_debugger() - cwd, testRunner, args = parse_argv() - run(cwd, testRunner, args) diff --git a/pythonFiles/visualstudio_py_testlauncher.py b/pythonFiles/visualstudio_py_testlauncher.py deleted file mode 100644 index 11fa053baaf0..000000000000 --- a/pythonFiles/visualstudio_py_testlauncher.py +++ /dev/null @@ -1,347 +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" - -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 "<stdout>" - else: - return "<stderr>" - - 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 [<option>] <test names>... ') - parser.add_option('--debug', action='store_true', help='Whether debugging the unit tests') - parser.add_option('-x', '--mixed-mode', action='store_true', help='wait for mixed-mode debugger to attach') - parser.add_option('-t', '--test', type='str', dest='tests', action='append', help='specifies a test to run') - parser.add_option('--testFile', type='str', help='Fully qualitified path to file name') - parser.add_option('-c', '--coverage', type='str', help='enable code coverage and specify filename') - parser.add_option('-r', '--result-port', type='int', help='connect to port on localhost and send test results') - parser.add_option('--us', type='str', help='Directory to start discovery') - parser.add_option('--up', type='str', help='Pattern to match test files (''test*.py'' default)') - parser.add_option('--ut', type='str', help='Top level directory of project (default to start directory)') - parser.add_option('--uvInt', '--verboseInt', type='int', help='Verbose output (0 none, 1 (no -v) simple, 2 (-v) full)') - parser.add_option('--uf', '--failfast', type='str', help='Stop on first failure') - parser.add_option('--uc', '--catch', type='str', help='Catch control-C and display results') - (opts, _) = parser.parse_args() - - if opts.debug: - from ptvsd.visualstudio_py_debugger import DONT_DEBUG, DEBUG_ENTRYPOINTS, get_code - - sys.path[0] = os.getcwd() - if opts.result_port: - try: - signal.signal(signal.SIGUSR1, signal_handler) - except: - try: - signal.signal(signal.SIGTERM, signal_handler) - except: - pass - _channel = _IpcChannel(socket.create_connection(('127.0.0.1', opts.result_port)), stopTests) - sys.stdout = _TestOutput(sys.stdout, is_stdout = True) - sys.stderr = _TestOutput(sys.stderr, is_stdout = False) - - if opts.debug: - DONT_DEBUG.append(os.path.normcase(__file__)) - DEBUG_ENTRYPOINTS.add(get_code(main)) - - pass - elif opts.mixed_mode: - # For mixed-mode attach, there's no ptvsd and hence no wait_for_attach(), - # so we have to use Win32 API in a loop to do the same thing. - from time import sleep - from ctypes import windll, c_char - while True: - if windll.kernel32.IsDebuggerPresent() != 0: - break - sleep(0.1) - try: - debugger_helper = windll['Microsoft.PythonTools.Debugger.Helper.x86.dll'] - except WindowsError: - debugger_helper = windll['Microsoft.PythonTools.Debugger.Helper.x64.dll'] - isTracing = c_char.in_dll(debugger_helper, "isTracing") - while True: - if isTracing.value != 0: - break - sleep(0.1) - - cov = None - try: - if opts.coverage: - try: - import coverage - cov = coverage.coverage(opts.coverage) - cov.load() - cov.start() - except: - pass - if opts.tests is None and opts.testFile is None: - if opts.us is None: - opts.us = '.' - if opts.up is None: - opts.up = 'test*.py' - tests = unittest.defaultTestLoader.discover(opts.us, opts.up) - else: - # loadTestsFromNames doesn't work well (with duplicate file names or class names) - # Easier approach is find the test suite and use that for running - loader = unittest.TestLoader() - # opts.us will be passed in - suites = loader.discover(opts.us, pattern=os.path.basename(opts.testFile)) - suite = None - tests = None - if opts.tests is None: - # Run everything in the test file - tests = suites - else: - # Run a specific test class or test method - for test_suite in suites._tests: - for cls in test_suite._tests: - try: - for m in cls._tests: - testId = m.id() - if testId.startswith(opts.tests[0]): - suite = cls - if testId == opts.tests[0]: - tests = unittest.TestSuite([m]) - break - except Exception as err: - errorMessage = traceback.format_exception() - pass - if tests is None: - tests = suite - if tests is None and suite is None: - _channel.send_event( - name='error', - outcome='', - traceback = '', - message = 'Failed to identify the test', - test = '' - ) - if opts.uvInt is None: - opts.uvInt = 0 - if opts.uf is not None: - runner = unittest.TextTestRunner(verbosity=opts.uvInt, resultclass=VsTestResult, failfast=True) - else: - runner = unittest.TextTestRunner(verbosity=opts.uvInt, resultclass=VsTestResult) - result = runner.run(tests) - if _channel is not None: - _channel.close() - sys.exit(not result.wasSuccessful()) - finally: - if cov is not None: - cov.stop() - cov.save() - cov.xml_report(outfile = opts.coverage + '.xml', omit=__file__) - if _channel is not None: - _channel.send_event( - name='done' - ) - _channel.socket.close() - # prevent generation of the error 'Error in sys.exitfunc:' - try: - sys.stdout.close() - except: - pass - try: - sys.stderr.close() - except: - pass - -if __name__ == '__main__': - main() diff --git a/python_files/.env b/python_files/.env new file mode 100644 index 000000000000..8ae3557bcd8d --- /dev/null +++ b/python_files/.env @@ -0,0 +1 @@ +PYTHONPATH=./lib/python diff --git a/python_files/.vscode/settings.json b/python_files/.vscode/settings.json new file mode 100644 index 000000000000..0f49d48f2e86 --- /dev/null +++ b/python_files/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "files.exclude": { + "**/__pycache__/**": true, + "**/**/*.pyc": true + }, + "python.formatting.provider": "black" +} diff --git a/python_files/Notebooks intro.ipynb b/python_files/Notebooks intro.ipynb new file mode 100644 index 000000000000..0e8aadad1919 --- /dev/null +++ b/python_files/Notebooks intro.ipynb @@ -0,0 +1,154 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Creating a new notebook" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "1. Open the command palette with the shortcut: `Ctrl/Command` + `Shift` + `P`\n", + "2. Search for the command `Create New Blank Notebook`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to get back to the start page" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "1. Open the command palette with the shortcut: `Ctrl/Command` + `Shift` + `P`\n", + "\n", + "2. Search for the command `Python: Open Start Page`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Getting started" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "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 double 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 ![code icon](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/codeIcon.PNG) /markdown ![markdown icon](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/markdownIcon.PNG) icons or using the keyboard shortcut `M` and `Y` respectively." + ] + }, + { + "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 ![play](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/playIcon.PNG) button in the cell toolbar, or use the keyboard shortcut `Ctrl/Command` + `Enter`.\n", + "* To edit the code, just click in cell and start editing.\n", + "* To add a new cell below, click the `Add Cell` icon ![add cell](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/addIcon.PNG) at the bottom left of the cell or enter command mode with the `ESC` Key and then use the keyboard shortcut `B` to create the new cell below.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Features" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Variable explorer**\n", + "\n", + "To view all your active variables and their current values in the notebook, click on the variable explorer icon ![variable explorer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/variableExplorerIcon.PNG) in the top toolbar.\n", + "\n", + "![Variable Explorer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/variableexplorer.png)\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 ![data viewer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/dataViewerIcon.PNG) which you can click to open the data viewer.\n", + "\n", + "![Data Viewer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/dataviewer.gif)\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 ![Export icon](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/exportIcon.PNG) in the top toolbar \n", + "\n", + "![Export](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/savetopythonfile.png)\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 ![Plot Viewer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/plotViewerIcon.PNG) 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", + "![Plot Viewer](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/plotviewer.gif)\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", + "![Switching Kernels](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/kernelchange.gif)\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", + "![Remote](https://raw.githubusercontent.com/microsoft/vscode-python/main/images/remoteserver.gif)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "-Rh3-Vt9Nev9" + }, + "source": [ + "# More Resources" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- [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)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.6 64-bit", + "metadata": { + "interpreter": { + "hash": "5c7437588f5ad65b3fb2510dff59138dda524824913550626013373b675d5274" + } + }, + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.8.6-final" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/python_files/create_conda.py b/python_files/create_conda.py new file mode 100644 index 000000000000..284f734081b2 --- /dev/null +++ b/python_files/create_conda.py @@ -0,0 +1,130 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import os +import pathlib +import subprocess +import sys +from typing import Optional, Sequence, Union + +CONDA_ENV_NAME = ".conda" +CWD = pathlib.Path.cwd() + + +class VenvError(Exception): + pass + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument( + "--python", + action="store", + help="Python version to install in the virtual environment.", + default=f"{sys.version_info.major}.{sys.version_info.minor}", + ) + parser.add_argument( + "--install", + action="store_true", + default=False, + help="Install packages into the virtual environment.", + ) + parser.add_argument( + "--git-ignore", + action="store_true", + default=False, + help="Add .gitignore to the newly created virtual environment.", + ) + parser.add_argument( + "--name", + default=CONDA_ENV_NAME, + type=str, + help="Name of the virtual environment.", + metavar="NAME", + action="store", + ) + return parser.parse_args(argv) + + +def file_exists(path: Union[str, pathlib.PurePath]) -> bool: + return os.path.exists(path) # noqa: PTH110 + + +def conda_env_exists(name: Union[str, pathlib.PurePath]) -> bool: + return os.path.exists(CWD / name) # noqa: PTH110 + + +def run_process(args: Sequence[str], error_message: str) -> None: + try: + print("Running: " + " ".join(args)) + subprocess.run(args, cwd=os.getcwd(), check=True) # noqa: PTH109 + except subprocess.CalledProcessError as exc: + raise VenvError(error_message) from exc + + +def get_conda_env_path(name: str) -> str: + return os.fspath(CWD / name) + + +def install_packages(env_path: str) -> None: + yml = os.fspath(CWD / "environment.yml") + if file_exists(yml): + print(f"CONDA_INSTALLING_YML: {yml}") + run_process( + [ + sys.executable, + "-m", + "conda", + "env", + "update", + "--prefix", + env_path, + "--file", + yml, + ], + "CREATE_CONDA.FAILED_INSTALL_YML", + ) + print("CREATE_CONDA.INSTALLED_YML") + + +def add_gitignore(name: str) -> None: + git_ignore = CWD / name / ".gitignore" + if not git_ignore.is_file(): + print(f"Creating: {os.fsdecode(git_ignore)}") + git_ignore.write_text("*") + + +def main(argv: Optional[Sequence[str]] = None) -> None: + if argv is None: + argv = [] + args = parse_args(argv) + + if conda_env_exists(args.name): + env_path = get_conda_env_path(args.name) + print(f"EXISTING_CONDA_ENV:{env_path}") + else: + run_process( + [ + sys.executable, + "-m", + "conda", + "create", + "--yes", + "--prefix", + args.name, + f"python={args.python}", + ], + "CREATE_CONDA.ENV_FAILED_CREATION", + ) + env_path = get_conda_env_path(args.name) + print(f"CREATED_CONDA_ENV:{env_path}") + if args.git_ignore: + add_gitignore(args.name) + + if args.install: + install_packages(env_path) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/python_files/create_microvenv.py b/python_files/create_microvenv.py new file mode 100644 index 000000000000..2f2135444bc1 --- /dev/null +++ b/python_files/create_microvenv.py @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import os +import pathlib +import subprocess +import sys +from typing import Optional, Sequence + +VENV_NAME = ".venv" +LIB_ROOT = pathlib.Path(__file__).parent / "lib" / "python" +CWD = pathlib.Path.cwd() + + +class MicroVenvError(Exception): + pass + + +def run_process(args: Sequence[str], error_message: str) -> None: + try: + print("Running: " + " ".join(args)) + subprocess.run(args, cwd=os.getcwd(), check=True) # noqa: PTH109 + except subprocess.CalledProcessError as exc: + raise MicroVenvError(error_message) from exc + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser() + + parser.add_argument( + "--name", + default=VENV_NAME, + type=str, + help="Name of the virtual environment.", + metavar="NAME", + action="store", + ) + return parser.parse_args(argv) + + +def create_microvenv(name: str): + run_process( + [sys.executable, os.fspath(LIB_ROOT / "microvenv.py"), name], + "CREATE_MICROVENV.MICROVENV_FAILED_CREATION", + ) + + +def main(argv: Optional[Sequence[str]] = None) -> None: + if argv is None: + argv = [] + args = parse_args(argv) + + print("CREATE_MICROVENV.CREATING_MICROVENV") + create_microvenv(args.name) + print("CREATE_MICROVENV.CREATED_MICROVENV") + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/python_files/create_venv.py b/python_files/create_venv.py new file mode 100644 index 000000000000..83106bd889f8 --- /dev/null +++ b/python_files/create_venv.py @@ -0,0 +1,271 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import importlib.util as import_util +import json +import os +import pathlib +import subprocess +import sys +import urllib.request as url_lib +from typing import List, Optional, Sequence, Union + +VENV_NAME = ".venv" +CWD = pathlib.Path.cwd() +MICROVENV_SCRIPT_PATH = pathlib.Path(__file__).parent / "create_microvenv.py" + + +class VenvError(Exception): + pass + + +def parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser() + + parser.add_argument( + "--requirements", + action="append", + default=[], + help="Install additional dependencies into the virtual environment.", + ) + + parser.add_argument( + "--toml", + action="store", + default=None, + help="Install additional dependencies from sources like `pyproject.toml` into the virtual environment.", + ) + + parser.add_argument( + "--extras", + action="append", + default=[], + help="Install specific package groups from `pyproject.toml` into the virtual environment.", + ) + + parser.add_argument( + "--git-ignore", + action="store_true", + default=False, + help="Add .gitignore to the newly created virtual environment.", + ) + + parser.add_argument( + "--name", + default=VENV_NAME, + type=str, + help="Name of the virtual environment.", + metavar="NAME", + action="store", + ) + + parser.add_argument( + "--stdin", + action="store_true", + default=False, + help="Read arguments from stdin.", + ) + + return parser.parse_args(argv) + + +def is_installed(module: str) -> bool: + return import_util.find_spec(module) is not None + + +def file_exists(path: Union[str, pathlib.PurePath]) -> bool: + return pathlib.Path(path).exists() + + +def is_file(path: Union[str, pathlib.PurePath]) -> bool: + return pathlib.Path(path).is_file() + + +def venv_exists(name: str) -> bool: + return ( + (CWD / name).exists() + and (CWD / name / "pyvenv.cfg").exists() + and file_exists(get_venv_path(name)) + ) + + +def run_process(args: Sequence[str], error_message: str) -> None: + try: + print("Running: " + " ".join(args)) + subprocess.run(args, cwd=os.getcwd(), check=True) # noqa: PTH109 + except subprocess.CalledProcessError as exc: + raise VenvError(error_message) from exc + + +def get_win_venv_path(name: str) -> str: + venv_dir = CWD / name + # If using MSYS2 Python, the Python executable is located in the 'bin' directory. + if file_exists(venv_dir / "bin" / "python.exe"): + return os.fspath(venv_dir / "bin" / "python.exe") + else: + return os.fspath(venv_dir / "Scripts" / "python.exe") + + +def get_venv_path(name: str) -> str: + # See `venv` doc here for more details on binary location: + # https://docs.python.org/3/library/venv.html#creating-virtual-environments + if sys.platform == "win32": + return get_win_venv_path(name) + else: + return os.fspath(CWD / name / "bin" / "python") + + +def install_requirements(venv_path: str, requirements: List[str]) -> None: + if not requirements: + return + + for requirement in requirements: + print(f"VENV_INSTALLING_REQUIREMENTS: {requirement}") + run_process( + [venv_path, "-m", "pip", "install", "-r", requirement], + "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS", + ) + print("CREATE_VENV.PIP_INSTALLED_REQUIREMENTS") + + +def install_toml(venv_path: str, extras: List[str]) -> None: + args = "." if len(extras) == 0 else f".[{','.join(extras)}]" + run_process( + [venv_path, "-m", "pip", "install", "-e", args], + "CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT", + ) + print("CREATE_VENV.PIP_INSTALLED_PYPROJECT") + + +def upgrade_pip(venv_path: str) -> None: + print("CREATE_VENV.UPGRADING_PIP") + run_process( + [venv_path, "-m", "pip", "install", "--upgrade", "pip"], + "CREATE_VENV.UPGRADE_PIP_FAILED", + ) + print("CREATE_VENV.UPGRADED_PIP") + + +def create_gitignore(git_ignore: Union[str, pathlib.PurePath]): + print("Creating:", os.fspath(git_ignore)) + pathlib.Path(git_ignore).write_text("*") + + +def add_gitignore(name: str) -> None: + git_ignore = CWD / name / ".gitignore" + if not is_file(git_ignore): + create_gitignore(git_ignore) + + +def download_pip_pyz(name: str): + url = "https://bootstrap.pypa.io/pip/pip.pyz" + print("CREATE_VENV.DOWNLOADING_PIP") + + try: + with url_lib.urlopen(url) as response: + pip_pyz_path = CWD / name / "pip.pyz" + pip_pyz_path.write_bytes(data=response.read()) + except Exception as exc: + raise VenvError("CREATE_VENV.DOWNLOAD_PIP_FAILED") from exc + + +def install_pip(name: str): + pip_pyz_path = os.fspath(CWD / name / "pip.pyz") + executable = get_venv_path(name) + print("CREATE_VENV.INSTALLING_PIP") + run_process( + [executable, pip_pyz_path, "install", "pip"], + "CREATE_VENV.INSTALL_PIP_FAILED", + ) + + +def get_requirements_from_args(args: argparse.Namespace) -> List[str]: + requirements = [] + if args.stdin: + data = json.loads(sys.stdin.read()) + requirements = data.get("requirements", []) + if args.requirements: + requirements.extend(args.requirements) + return requirements + + +def main(argv: Optional[Sequence[str]] = None) -> None: + if argv is None: + argv = [] + args = parse_args(argv) + + use_micro_venv = False + venv_installed = is_installed("venv") + pip_installed = is_installed("pip") + ensure_pip_installed = is_installed("ensurepip") + distutils_installed = is_installed("distutils") + + if not venv_installed: + if sys.platform == "win32": + raise VenvError("CREATE_VENV.VENV_NOT_FOUND") + else: + use_micro_venv = True + if not distutils_installed: + print("Install `python3-distutils` package or equivalent for your OS.") + print("On Debian/Ubuntu: `sudo apt install python3-distutils`") + raise VenvError("CREATE_VENV.DISTUTILS_NOT_INSTALLED") + + if venv_exists(args.name): + # A virtual environment with same name exists. + # We will use the existing virtual environment. + venv_path = get_venv_path(args.name) + print(f"EXISTING_VENV:{venv_path}") + else: + if use_micro_venv: + # `venv` was not found but on this platform we can use `microvenv` + run_process( + [ + sys.executable, + os.fspath(MICROVENV_SCRIPT_PATH), + "--name", + args.name, + ], + "CREATE_VENV.MICROVENV_FAILED_CREATION", + ) + elif not pip_installed or not ensure_pip_installed: + # `venv` was found but `pip` or `ensurepip` was not found. + # We create a venv without `pip` in it. We will later install `pip`. + run_process( + [sys.executable, "-m", "venv", "--without-pip", args.name], + "CREATE_VENV.VENV_FAILED_CREATION", + ) + else: + # Both `venv` and `pip` were found. So create a .venv normally + run_process( + [sys.executable, "-m", "venv", args.name], + "CREATE_VENV.VENV_FAILED_CREATION", + ) + + venv_path = get_venv_path(args.name) + print(f"CREATED_VENV:{venv_path}") + + if args.git_ignore: + add_gitignore(args.name) + + # At this point we have a .venv. Now we handle installing `pip`. + if pip_installed and ensure_pip_installed: + # We upgrade pip if it is already installed. + upgrade_pip(venv_path) + else: + # `pip` was not found, so we download it and install it. + download_pip_pyz(args.name) + install_pip(args.name) + + requirements = get_requirements_from_args(args) + if requirements: + print(f"VENV_INSTALLING_REQUIREMENTS: {requirements}") + install_requirements(venv_path, requirements) + + if args.toml: + print(f"VENV_INSTALLING_PYPROJECT: {args.toml}") + install_toml(venv_path, args.extras) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/python_files/deactivate/bash/deactivate b/python_files/deactivate/bash/deactivate new file mode 100755 index 000000000000..f6dd33425d1a --- /dev/null +++ b/python_files/deactivate/bash/deactivate @@ -0,0 +1,44 @@ +# Same as deactivate in "<venv>/bin/activate" +deactivate () { + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + unset -f deactivate + fi +} + +# Get the directory of the current script +SCRIPT_DIR=$(dirname "$0") +# Construct the path to envVars.txt relative to the script directory +ENV_FILE="$SCRIPT_DIR/envVars.txt" + +# Read the JSON file and set the variables +TEMP_PS1=$(grep '^PS1=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PATH=$(grep '^PATH=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PYTHONHOME=$(grep '^PYTHONHOME=' $ENV_FILE | cut -d '=' -f 2) +# Initialize the variables required by deactivate function +_OLD_VIRTUAL_PS1="${TEMP_PS1:-}" +_OLD_VIRTUAL_PATH="$TEMP_PATH" +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${TEMP_PYTHONHOME:-}" +fi +deactivate +bash diff --git a/python_files/deactivate/fish/deactivate b/python_files/deactivate/fish/deactivate new file mode 100755 index 000000000000..3a9d50ccde2b --- /dev/null +++ b/python_files/deactivate/fish/deactivate @@ -0,0 +1,44 @@ +# Same as deactivate in "<venv>/bin/activate" +deactivate () { + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + unset -f deactivate + fi +} + +# Get the directory of the current script +SCRIPT_DIR=$(dirname "$0") +# Construct the path to envVars.txt relative to the script directory +ENV_FILE="$SCRIPT_DIR/envVars.txt" + +# Read the JSON file and set the variables +TEMP_PS1=$(grep '^PS1=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PATH=$(grep '^PATH=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PYTHONHOME=$(grep '^PYTHONHOME=' $ENV_FILE | cut -d '=' -f 2) +# Initialize the variables required by deactivate function +_OLD_VIRTUAL_PS1="${TEMP_PS1:-}" +_OLD_VIRTUAL_PATH="$TEMP_PATH" +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${TEMP_PYTHONHOME:-}" +fi +deactivate +fish diff --git a/python_files/deactivate/powershell/deactivate.ps1 b/python_files/deactivate/powershell/deactivate.ps1 new file mode 100644 index 000000000000..49365e0fbeff --- /dev/null +++ b/python_files/deactivate/powershell/deactivate.ps1 @@ -0,0 +1,11 @@ +# Load dotenv-style file and restore environment variables +Get-Content -Path "$PSScriptRoot\envVars.txt" | ForEach-Object { + # Split each line into key and value at the first '=' + $parts = $_ -split '=', 2 + if ($parts.Count -eq 2) { + $key = $parts[0].Trim() + $value = $parts[1].Trim() + # Set the environment variable + Set-Item -Path "env:$key" -Value $value + } +} diff --git a/python_files/deactivate/zsh/deactivate b/python_files/deactivate/zsh/deactivate new file mode 100755 index 000000000000..8b059318f988 --- /dev/null +++ b/python_files/deactivate/zsh/deactivate @@ -0,0 +1,44 @@ +# Same as deactivate in "<venv>/bin/activate" +deactivate () { + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + unset -f deactivate + fi +} + +# Get the directory of the current script +SCRIPT_DIR=$(dirname "$0") +# Construct the path to envVars.txt relative to the script directory +ENV_FILE="$SCRIPT_DIR/envVars.txt" + +# Read the JSON file and set the variables +TEMP_PS1=$(grep '^PS1=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PATH=$(grep '^PATH=' $ENV_FILE | cut -d '=' -f 2) +TEMP_PYTHONHOME=$(grep '^PYTHONHOME=' $ENV_FILE | cut -d '=' -f 2) +# Initialize the variables required by deactivate function +_OLD_VIRTUAL_PS1="${TEMP_PS1:-}" +_OLD_VIRTUAL_PATH="$TEMP_PATH" +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${TEMP_PYTHONHOME:-}" +fi +deactivate +zsh diff --git a/python_files/download_get_pip.py b/python_files/download_get_pip.py new file mode 100644 index 000000000000..91ab107760d8 --- /dev/null +++ b/python_files/download_get_pip.py @@ -0,0 +1,59 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import pathlib +import urllib.request as url_lib + +from packaging.version import parse as version_parser + +EXTENSION_ROOT = pathlib.Path(__file__).parent.parent +GET_PIP_DEST = EXTENSION_ROOT / "python_files" +PIP_PACKAGE = "pip" +PIP_VERSION = "latest" # Can be "latest", or specific version "23.1.2" + + +def _get_package_data(): + json_uri = f"https://pypi.org/pypi/{PIP_PACKAGE}/json" + # 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 _download_and_save(root, version): + root = pathlib.Path.cwd() if root is None or root == "." else pathlib.Path(root) + url = f"https://raw.githubusercontent.com/pypa/get-pip/{version}/public/get-pip.py" + print(url) + with url_lib.urlopen(url) as response: + data = response.read() + get_pip_file = root / "get-pip.py" + get_pip_file.write_bytes(data) + + +def main(root): + data = _get_package_data() + + if PIP_VERSION == "latest": + # Pick latest 5 versions to try and get-pip + sorted_versions = sorted(data["releases"].keys(), key=version_parser, reverse=True)[:5] + downloaded = False + while sorted_versions: + use_version = sorted_versions.pop(0) + try: + print(f"Trying version: get-pip == {use_version}") + _download_and_save(root, use_version) + downloaded = True + break + except Exception as e: + print(f"Failed to download get-pip == {use_version}: {e}") + print(f"NExt attempt(s) with versions: {sorted_versions}") + if not downloaded: + raise Exception("Failed to download get-pip.py") + else: + use_version = PIP_VERSION + _download_and_save(root, use_version) + + +if __name__ == "__main__": + main(GET_PIP_DEST) diff --git a/python_files/get_output_via_markers.py b/python_files/get_output_via_markers.py new file mode 100644 index 000000000000..e37f7f8c5df0 --- /dev/null +++ b/python_files/get_output_via_markers.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import runpy +import sys + +# Sometimes executing scripts can print out stuff before the actual output is +# printed. For eg. when activating conda. Hence, printing out markers to make +# it more resilient to pull the output. +print(">>>PYTHON-EXEC-OUTPUT") + +module = sys.argv[1] +try: + if module == "-c": + ns = {} + code = sys.argv[2] + del sys.argv[2] + del sys.argv[0] + exec(code, ns, ns) + elif module.startswith("-m"): + module_name = sys.argv[2] + sys.argv = sys.argv[2:] # It should begin with the module name. + runpy.run_module(module_name, run_name="__main__", alter_sys=True) + elif module.endswith(".py"): + sys.argv = sys.argv[1:] + runpy.run_path(module, run_name="__main__") + elif module.startswith("-"): + raise NotImplementedError(sys.argv) + else: + runpy.run_module(module, run_name="__main__", alter_sys=True) +finally: + print("<<<PYTHON-EXEC-OUTPUT") diff --git a/python_files/get_variable_info.py b/python_files/get_variable_info.py new file mode 100644 index 000000000000..d60795982617 --- /dev/null +++ b/python_files/get_variable_info.py @@ -0,0 +1,539 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +import locale +import sys +from typing import ClassVar + + +# this class is from in ptvsd/debugpy tools +class SafeRepr(object): # noqa: UP004 + # 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 = 128 + string_types = (str, bytes) + bytes = bytes + set_info = (set, "{", "}", False) + frozenset_info = (frozenset, "frozenset({", "})", False) + int_types = (int,) + long_iter_types = (list, tuple, bytearray, range, dict, set, frozenset) + + # Collection types are recursively iterated for each limit in + # maxcollection. + maxcollection = (60, 20) + + # 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 = [ # noqa: RUF012 + (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: ClassVar[list] = [(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 = 128 + + convert_to_hex = False + raw_value = False + + def __call__(self, obj): + """ + :param object obj: + The object for which we want a representation. + + :return str: + Returns bytes encoded as utf-8 on py2 and str on py3. + """ # noqa: D205 + try: + return "".join(self._repr(obj, 0)) + except Exception: + try: + return f"An exception was raised: {sys.exc_info()[1]!r}" + 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: + 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 + + # range reprs fine regardless of length. + if isinstance(obj, range): + 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) + return any( + i > self.maxcollection[level] or self._is_long_iter(item, level + 1) + for i, item in enumerate(obj) + ) + + except Exception: + # If anything breaks, assume the worst case. + return True + + def _repr_iter(self, obj, level, prefix, suffix, comma_after_single_element=False): # noqa: FBT002 + 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 + + yield from self._repr(item, 100 if item is obj else level + 1) + else: + if comma_after_single_element: # noqa: SIM102 + 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 = f"<{type(obj).__name__}, len() = {length}>" + except Exception: + try: + obj_repr = "<" + type(obj).__name__ + ">" + except Exception: + obj_repr = "<no repr available for object>" + 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 + + obj_keys = list(obj) + + for key in obj_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): + try: + if self.raw_value: + # For raw value retrieval, ignore all limits. + if isinstance(obj, bytes): + yield obj.decode("latin-1") + else: + yield obj + return + + limit_inner = self.maxother_inner + limit_outer = self.maxother_outer + limit = limit_inner if level > 0 else limit_outer + if len(obj) <= limit: + # Note that we check the limit before doing the repr (so, the final string + # may actually be considerably bigger on some cases, as besides + # the additional u, b, ' chars, some chars may be escaped in repr, so + # even a single char such as \U0010ffff may end up adding more + # chars than expected). + yield self._convert_to_unicode_or_bytes_repr(repr(obj)) + return + + # Slightly imprecise calculations - we may end up with a string that is + # up to 6 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)) + + # Important: only do repr after slicing to avoid duplicating a byte array that could be + # huge. + + # Note: we don't deal with high surrogates here because we're not dealing with the + # repr() of a random object. + # i.e.: A high surrogate unicode char may be splitted on Py2, but as we do a `repr` + # afterwards, that's ok. + + # Also, we just show the unicode/string/bytes repr() directly to make clear what the + # input type was (so, on py2 a unicode would start with u' and on py3 a bytes would + # start with b'). + + part1 = obj[:left_count] + part1 = repr(part1) + part1 = part1[: part1.rindex("'")] # Remove the last ' + + part2 = obj[-right_count:] + part2 = repr(part2) + part2 = part2[part2.index("'") + 1 :] # Remove the first ' (and possibly u or b). + + yield part1 + yield "..." + yield part2 + except: # noqa: E722 + # This shouldn't really happen, but let's play it safe. + # exception('Error getting string representation to show.') + yield from self._repr_obj(obj, level, self.maxother_inner, self.maxother_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 = "<no repr available for " + type(obj).__name__ + ">" + except Exception: + obj_repr = "<no repr available for object>" + + 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)) + + yield obj_repr[:left_count] + yield "..." + yield obj_repr[-right_count:] + + def _convert_to_unicode_or_bytes_repr(self, 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(sys.stdout, "encoding", None) + if encoding: + try_encodings.append(encoding.lower()) + + preferred_encoding = self.locale_preferred_encoding or 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: # noqa: PERF203 + pass + + return obj_repr # Return the original version (in bytes) + + +class DisplayOptions: + def __init__(self, width, max_columns): + self.width = width + self.max_columns = max_columns + + +_safe_repr = SafeRepr() +_collection_types = ["list", "tuple", "set"] +_array_page_size = 50 + + +def _get_value(variable): + return _safe_repr(variable) + + +def _get_property_names(variable): + props = [] + private_props = [] + for prop in dir(variable): + if not prop.startswith("_"): + props.append(prop) + elif not prop.startswith("__"): + private_props.append(prop) + return props + private_props + + +def _get_full_type(var_type): + module = "" + if hasattr(var_type, "__module__") and var_type.__module__ != "builtins": + module = var_type.__module__ + "." + if hasattr(var_type, "__qualname__"): + return module + var_type.__qualname__ + elif hasattr(var_type, "__name__"): + return module + var_type.__name__ + return None + + +def _get_variable_description(variable): + result = {} + + var_type = type(variable) + result["type"] = _get_full_type(var_type) + if hasattr(var_type, "__mro__"): + result["interfaces"] = [_get_full_type(t) for t in var_type.__mro__] + + if hasattr(variable, "__len__") and result["type"] in _collection_types: + result["count"] = len(variable) + + result["hasNamedChildren"] = hasattr(variable, "__dict__") or isinstance(variable, dict) + + result["value"] = _get_value(variable) + return result + + +def _get_child_property(root, property_chain): + try: + variable = root + for prop in property_chain: + if isinstance(prop, int): + if hasattr(variable, "__getitem__"): + variable = variable[prop] + elif isinstance(variable, set): + variable = list(variable)[prop] + else: + return None + elif hasattr(variable, prop): + variable = getattr(variable, prop) + elif isinstance(variable, dict) and prop in variable: + variable = variable[prop] + else: + return None + except Exception: + return None + + return variable + + +types_to_exclude = ["module", "function", "method", "class", "type"] + + +### Get info on variables at the root level +def getVariableDescriptions(): # noqa: N802 + return [ + { + "name": varName, + **_get_variable_description(globals()[varName]), + "root": varName, + "propertyChain": [], + "language": "python", + } + for varName in globals() + if type(globals()[varName]).__name__ not in types_to_exclude + and not varName.startswith("__") + ] + + +### Get info on children of a variable reached through the given property chain +def getAllChildrenDescriptions(root_var_name, property_chain, start_index): # noqa: N802 + root = globals()[root_var_name] + if root is None: + return [] + + parent = root + if len(property_chain) > 0: + parent = _get_child_property(root, property_chain) + + children = [] + parent_info = _get_variable_description(parent) + if "count" in parent_info: + if parent_info["count"] > 0: + last_item = min(parent_info["count"], start_index + _array_page_size) + index_range = range(start_index, last_item) + children = [ + { + **_get_variable_description(_get_child_property(parent, [i])), + "name": str(i), + "root": root_var_name, + "propertyChain": [*property_chain, i], + "language": "python", + } + for i in index_range + ] + elif parent_info["hasNamedChildren"]: + children_names = [] + if hasattr(parent, "__dict__"): + children_names = _get_property_names(parent) + elif isinstance(parent, dict): + children_names = list(parent.keys()) + + children = [] + for prop in children_names: + child_property = _get_child_property(parent, [prop]) + if child_property is not None and type(child_property).__name__ not in types_to_exclude: + child = { + **_get_variable_description(child_property), + "name": prop, + "root": root_var_name, + "propertyChain": [*property_chain, prop], + } + children.append(child) + + return children diff --git a/python_files/installed_check.py b/python_files/installed_check.py new file mode 100644 index 000000000000..4fa3cdbb2385 --- /dev/null +++ b/python_files/installed_check.py @@ -0,0 +1,132 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import json +import os +import pathlib +import sys +from typing import Dict, List, Optional, Sequence, Tuple, Union + +LIB_ROOT = pathlib.Path(__file__).parent / "lib" / "python" +sys.path.insert(0, os.fspath(LIB_ROOT)) + +import tomli # noqa: E402 +from importlib_metadata import metadata # noqa: E402 +from packaging.requirements import Requirement # noqa: E402 + +DEFAULT_SEVERITY = "3" # 'Hint' +try: + SEVERITY = int(os.getenv("VSCODE_MISSING_PGK_SEVERITY", DEFAULT_SEVERITY)) +except ValueError: + SEVERITY = int(DEFAULT_SEVERITY) + + +def parse_args(argv: Optional[Sequence[str]] = None): + if argv is None: + argv = sys.argv[1:] + parser = argparse.ArgumentParser( + description="Check for installed packages against requirements" + ) + parser.add_argument("FILEPATH", type=str, help="Path to requirements.[txt, in]") + + return parser.parse_args(argv) + + +def parse_requirements(line: str) -> Optional[Requirement]: + try: + req = Requirement(line.strip("\\")) + if req.marker is None or req.marker.evaluate(): + return req + except Exception: + pass + return None + + +def process_requirements(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]: + diagnostics = [] + for n, line in enumerate(req_file.read_text(encoding="utf-8").splitlines()): + if line.startswith(("#", "-", " ")) or line == "": + continue + + req = parse_requirements(line) + if req: + try: + # Check if package is installed + metadata(req.name) + except Exception: + diagnostics.append( + { + "line": n, + "character": 0, + "endLine": n, + "endCharacter": len(req.name), + "package": req.name, + "code": "not-installed", + "severity": SEVERITY, + } + ) + return diagnostics + + +def get_pos(lines: List[str], text: str) -> Tuple[int, int, int, int]: + for n, line in enumerate(lines): + index = line.find(text) + if index >= 0: + return n, index, n, index + len(text) + return (0, 0, 0, 0) + + +def process_pyproject(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]: + diagnostics = [] + try: + raw_text = req_file.read_text(encoding="utf-8") + pyproject = tomli.loads(raw_text) + except Exception: + return diagnostics + + lines = raw_text.splitlines() + reqs = pyproject.get("project", {}).get("dependencies", []) + for raw_req in reqs: + req = parse_requirements(raw_req) + n, start, _, end = get_pos(lines, raw_req) + if req: + try: + # Check if package is installed + metadata(req.name) + except Exception: + diagnostics.append( + { + "line": n, + "character": start, + "endLine": n, + "endCharacter": end, + "package": req.name, + "code": "not-installed", + "severity": SEVERITY, + } + ) + return diagnostics + + +def get_diagnostics(req_file: pathlib.Path) -> List[Dict[str, Union[str, int]]]: + diagnostics = [] + if not req_file.exists(): + return diagnostics + + if req_file.name == "pyproject.toml": + diagnostics = process_pyproject(req_file) + else: + diagnostics = process_requirements(req_file) + + return diagnostics + + +def main(): + args = parse_args() + diagnostics = get_diagnostics(pathlib.Path(args.FILEPATH)) + print(json.dumps(diagnostics, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/python_files/interpreterInfo.py b/python_files/interpreterInfo.py new file mode 100644 index 000000000000..f15da9e48ea3 --- /dev/null +++ b/python_files/interpreterInfo.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import sys + +obj = {} +obj["versionInfo"] = tuple(sys.version_info) +obj["sysPrefix"] = sys.prefix +obj["sysVersion"] = sys.version +obj["is64Bit"] = sys.maxsize > 2**32 + +print(json.dumps(obj)) diff --git a/python_files/jedilsp_requirements/requirements.in b/python_files/jedilsp_requirements/requirements.in new file mode 100644 index 000000000000..794e9c8ea686 --- /dev/null +++ b/python_files/jedilsp_requirements/requirements.in @@ -0,0 +1,8 @@ +# This file is used to generate requirements.txt. +# To update requirements.txt, run the following commands. +# Use Python 3.9 when creating the environment or using pip-tools +# 1) Install `uv` https://docs.astral.sh/uv/getting-started/installation/ +# 2) uv pip compile --generate-hashes --upgrade python_files\jedilsp_requirements\requirements.in -o python_files\jedilsp_requirements\requirements.txt + +jedi-language-server>=0.34.3 +pygls>=0.10.3 diff --git a/python_files/jedilsp_requirements/requirements.txt b/python_files/jedilsp_requirements/requirements.txt new file mode 100644 index 000000000000..e2599e7bbce4 --- /dev/null +++ b/python_files/jedilsp_requirements/requirements.txt @@ -0,0 +1,63 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --generate-hashes python_files\jedilsp_requirements\requirements.in -o .\python_files\jedilsp_requirements\requirements.txt +attrs==25.3.0 \ + --hash=sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 \ + --hash=sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b + # via + # cattrs + # lsprotocol +cattrs==25.2.0 \ + --hash=sha256:539d7eedee7d2f0706e4e109182ad096d608ba84633c32c75ef3458f1d11e8f1 \ + --hash=sha256:f46c918e955db0177be6aa559068390f71988e877c603ae2e56c71827165cc06 + # via + # jedi-language-server + # lsprotocol + # pygls +docstring-to-markdown==0.17 \ + --hash=sha256:df72a112294c7492487c9da2451cae0faeee06e86008245c188c5761c9590ca3 \ + --hash=sha256:fd7d5094aa83943bf5f9e1a13701866b7c452eac19765380dead666e36d3711c + # via jedi-language-server +exceptiongroup==1.3.0 \ + --hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \ + --hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88 + # via cattrs +importlib-metadata==8.7.0 \ + --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ + --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd + # via docstring-to-markdown +jedi==0.19.2 \ + --hash=sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0 \ + --hash=sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9 + # via jedi-language-server +jedi-language-server==0.45.1 \ + --hash=sha256:8c0c6b4eaeffdbb87be79e9897c9929ffeddf875dff7c1c36dd67768e294942b \ + --hash=sha256:a1fcfba8008f2640e921937fcf1933c3961d74249341eba8b3ef9a0c3f817102 + # via -r python_files/jedilsp_requirements/requirements.in +lsprotocol==2023.0.1 \ + --hash=sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2 \ + --hash=sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d + # via + # jedi-language-server + # pygls +parso==0.8.5 \ + --hash=sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a \ + --hash=sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887 + # via jedi +pygls==1.3.1 \ + --hash=sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018 \ + --hash=sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e + # via + # -r python_files/jedilsp_requirements/requirements.in + # jedi-language-server +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via + # cattrs + # docstring-to-markdown + # exceptiongroup + # jedi-language-server +zipp==3.23.0 \ + --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ + --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 + # via importlib-metadata diff --git a/python_files/linter.py b/python_files/linter.py new file mode 100644 index 000000000000..edbbe9dfafe5 --- /dev/null +++ b/python_files/linter.py @@ -0,0 +1,51 @@ +import subprocess +import sys + +linter_settings = { + "pylint": { + "args": ["--reports=n", "--output-format=json"], + }, + "flake8": { + "args": ["--format", "%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s"], + }, + "bandit": { + "args": [ + "-f", + "custom", + "--msg-template", + "{line},{col},{severity},{test_id}:{msg}", + "-n", + "-1", + ], + }, + "mypy": {"args": []}, + "prospector": { + "args": ["--absolute-paths", "--output-format=json"], + }, + "pycodestyle": { + "args": ["--format", "%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s"], + }, + "pydocstyle": { + "args": [], + }, + "pylama": {"args": ["--format=parsable"]}, +} + + +def main(): + invoke = sys.argv[1] + if invoke == "-m": + linter = sys.argv[2] + args = [sys.executable, "-m", linter] + linter_settings[linter]["args"] + sys.argv[3:] + else: + linter = sys.argv[2] + args = [sys.argv[3]] + linter_settings[linter]["args"] + sys.argv[4:] + + if hasattr(subprocess, "run"): + subprocess.run(args, encoding="utf-8", stdout=sys.stdout, stderr=sys.stderr) + else: + subprocess.call(args, stdout=sys.stdout, stderr=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/python_files/normalizeSelection.py b/python_files/normalizeSelection.py new file mode 100644 index 000000000000..9d82a4dc9440 --- /dev/null +++ b/python_files/normalizeSelection.py @@ -0,0 +1,310 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import ast +import json +import re +import sys +import textwrap +from typing import Iterable + +attach_bracket_paste = sys.version_info >= (3, 13) + + +def split_lines(source): + """ + Split selection lines in a version-agnostic way. + + Python grammar only treats \r, \n, and \r\n as newlines. + But splitlines() in Python 3 has a much larger list: for example, it also includes \v, \f. + As such, this function will split lines across all Python versions. + """ + return re.split(r"[\n\r]+", source) + + +def _get_statements(selection): + """Process a multiline selection into a list of its top-level statements. + + This will remove empty newlines around and within the selection, dedent it, + and split it using the result of `ast.parse()`. + """ + # Remove blank lines within the selection to prevent the REPL from thinking the block is finished. + lines = (line for line in split_lines(selection) if line.strip() != "") + + # Dedent the selection and parse it using the ast module. + # Note that leading comments in the selection will be discarded during parsing. + source = textwrap.dedent("\n".join(lines)) + tree = ast.parse(source) + + # We'll need the dedented lines to rebuild the selection. + lines = split_lines(source) + + # Get the line ranges for top-level blocks returned from parsing the dedented text + # and split the selection accordingly. + # tree.body is a list of AST objects, which we rely on to extract top-level statements. + # If we supported Python 3.8+ only we could use the lineno and end_lineno attributes of each object + # to get the boundaries of each block. + # However, earlier Python versions only have the lineno attribute, which is the range start position (1-indexed). + # Therefore, to retrieve the end line of each block in a version-agnostic way we need to do + # `end = next_block.lineno - 1` + # for all blocks except the last one, which will will just run until the last line. + ends = [] + for node in tree.body[1:]: + line_end = node.lineno - 1 + # Special handling of decorators: + # In Python 3.8 and higher, decorators are not taken into account in the value returned by lineno, + # and we have to use the length of the decorator_list array to compute the actual start line. + # Before that, lineno takes into account decorators, so this offset check is unnecessary. + # Also, not all AST objects can have decorators. + if hasattr(node, "decorator_list") and sys.version_info >= (3, 8): + # Using getattr instead of node.decorator_list or pyright will complain about an unknown member. + line_end -= len(getattr(node, "decorator_list")) # noqa: B009 + ends.append(line_end) + ends.append(len(lines)) + + for node, end in zip(tree.body, ends): + # Given this selection: + # 1: if (m > 0 and + # 2: n < 3): + # 3: print('foo') + # 4: value = 'bar' + # + # The first block would have lineno = 1,and the second block lineno = 4 + start = node.lineno - 1 + + # Special handling of decorators similar to what's above. + if hasattr(node, "decorator_list") and sys.version_info >= (3, 8): + # Using getattr instead of node.decorator_list or pyright will complain about an unknown member. + start -= len(getattr(node, "decorator_list")) # noqa: B009 + block = "\n".join(lines[start:end]) + + # If the block is multiline, add an extra newline character at its end. + # This way, when joining blocks back together, there will be a blank line between each multiline statement + # and no blank lines between single-line statements, or it would look like this: + # >>> x = 22 + # >>> + # >>> total = x + 30 + # >>> + # Note that for the multiline parentheses case this newline is redundant, + # since the closing parenthesis terminates the statement already. + # This means that for this pattern we'll end up with: + # >>> x = [ + # ... 1 + # ... ] + # >>> + # >>> y = [ + # ... 2 + # ...] + if end - start > 1: + block += "\n" + + yield block + + +def normalize_lines(selection): + """ + Normalize the text selection received from the extension. + + If it is a single line selection, dedent it and append a newline and + send it back to the extension. + Otherwise, sanitize the multiline selection before returning it: + split it in a list of top-level statements + and add newlines between each of them so the REPL knows where each block ends. + """ + try: + # Parse the selection into a list of top-level blocks. + # We don't differentiate between single and multiline statements + # because it's not a perf bottleneck, + # and the overhead from splitting and rejoining strings in the multiline case is one-off. + statements = _get_statements(selection) + + # Insert a newline between each top-level statement, and append a newline to the selection. + source = "\n".join(statements) + "\n" + # If selection ends with trailing dictionary or list, remove last unnecessary newline. + if selection[-2] == "}" or selection[-2] == "]": + source = source[:-1] + # If the selection contains trailing return dictionary, insert newline to trigger execute. + if check_end_with_return_dict(selection): + source = source + "\n" + except Exception: + # If there's a problem when parsing statements, + # append a blank line to end the block and send it as-is. + source = selection + "\n\n" + + return source + + +top_level_nodes = [] +min_key = None + + +def check_end_with_return_dict(code): + stripped_code = code.strip() + return stripped_code.endswith("}") and "return {" in stripped_code.strip() + + +def check_exact_exist(top_level_nodes, start_line, end_line): + return [ + node + for node in top_level_nodes + if node.lineno == start_line and node.end_lineno == end_line + ] + + +def traverse_file(whole_file_content, start_line, end_line, was_highlighted): # noqa: ARG001 + """Intended to traverse through a user's given file content and find, collect all appropriate lines that should be sent to the REPL in case of smart selection. + + This could be exact statement such as just a single line print statement, + or a multiline dictionary, or differently styled multi-line list comprehension, etc. + Then call the normalize_lines function to normalize our smartly selected code block. + """ + parsed_file_content = None + + try: + parsed_file_content = ast.parse(whole_file_content) + except Exception: + # Handle case where user is attempting to run code where file contains deprecated Python code. + # Let typescript side know and show warning message. + return { + "normalized_smart_result": "deprecated", + "which_line_next": 0, + } + + smart_code = "" + should_run_top_blocks = [] + + # Purpose of this loop is to fetch and collect all the + # AST top level nodes, and its node.body as child nodes. + # Individual nodes will contain information like + # the start line, end line and get source segment information + # that will be used to smartly select, and send normalized code. + for node in ast.iter_child_nodes(parsed_file_content): + top_level_nodes.append(node) + + ast_types_with_nodebody = ( + ast.Module, + ast.Interactive, + ast.Expression, + ast.FunctionDef, + ast.AsyncFunctionDef, + ast.ClassDef, + ast.For, + ast.AsyncFor, + ast.While, + ast.If, + ast.With, + ast.AsyncWith, + ast.Try, + ast.Lambda, + ast.IfExp, + ast.ExceptHandler, + ) + if isinstance(node, ast_types_with_nodebody) and isinstance(node.body, Iterable): + top_level_nodes.extend(node.body) + + exact_nodes = check_exact_exist(top_level_nodes, start_line, end_line) + + # Just return the exact top level line, if present. + if len(exact_nodes) > 0: + which_line_next = 0 + for same_line_node in exact_nodes: + should_run_top_blocks.append(same_line_node) + smart_code += f"{ast.get_source_segment(whole_file_content, same_line_node)}\n" + which_line_next = get_next_block_lineno(should_run_top_blocks) + return { + "normalized_smart_result": smart_code, + "which_line_next": which_line_next, + } + + # For each of the nodes in the parsed file content, + # add the appropriate source code line(s) to be sent to the REPL, dependent on + # user is trying to send and execute single line/statement or multiple with smart selection. + for top_node in ast.iter_child_nodes(parsed_file_content): + if start_line == top_node.lineno and end_line == top_node.end_lineno: + should_run_top_blocks.append(top_node) + + smart_code += f"{ast.get_source_segment(whole_file_content, top_node)}\n" + break # If we found exact match, don't waste computation in parsing extra nodes. + elif start_line >= top_node.lineno and end_line <= top_node.end_lineno: + # Case to apply smart selection for multiple line. + # This is the case for when we have to add multiple lines that should be included in the smart send. + # For example: + # 'my_dictionary': { + # 'Audi': 'Germany', + # 'BMW': 'Germany', + # 'Genesis': 'Korea', + # } + # with the mouse cursor at 'BMW': 'Germany', should send all of the lines that pertains to my_dictionary. + + should_run_top_blocks.append(top_node) + + smart_code += str(ast.get_source_segment(whole_file_content, top_node)) + smart_code += "\n" + + normalized_smart_result = normalize_lines(smart_code) + which_line_next = get_next_block_lineno(should_run_top_blocks) + return { + "normalized_smart_result": normalized_smart_result, + "which_line_next": which_line_next, + } + + +# Look at the last top block added, find lineno for the next upcoming block, +# This will be used in calculating lineOffset to move cursor in VS Code. +def get_next_block_lineno(which_line_next): + last_ran_lineno = int(which_line_next[-1].end_lineno) + next_lineno = int(which_line_next[-1].end_lineno) + + for reverse_node in top_level_nodes: + if reverse_node.lineno > last_ran_lineno: + next_lineno = reverse_node.lineno + break + return next_lineno + + +if __name__ == "__main__": + # Content is being sent from the extension as a JSON object. + # Decode the data from the raw bytes. + stdin = sys.stdin if sys.version_info < (3,) else sys.stdin.buffer + raw = stdin.read() + contents = json.loads(raw.decode("utf-8")) + # Empty highlight means user has not explicitly selected specific text. + empty_highlight = contents.get("emptyHighlight", False) + + # We also get the activeEditor selection start line and end line from the typescript VS Code side. + # Remember to add 1 to each of the received since vscode starts line counting from 0 . + vscode_start_line = contents["startLine"] + 1 + vscode_end_line = contents["endLine"] + 1 + + # Send the normalized code back to the extension in a JSON object. + data = None + which_line_next = 0 + + if empty_highlight and contents.get("smartSendSettingsEnabled"): + result = traverse_file( + contents["wholeFileContent"], + vscode_start_line, + vscode_end_line, + not empty_highlight, + ) + normalized = result["normalized_smart_result"] + which_line_next = result["which_line_next"] + if normalized == "deprecated": + data = json.dumps( + {"normalized": normalized, "attach_bracket_paste": attach_bracket_paste} + ) + else: + data = json.dumps( + { + "normalized": normalized, + "nextBlockLineno": result["which_line_next"], + "attach_bracket_paste": attach_bracket_paste, + } + ) + else: + normalized = normalize_lines(contents["code"]) + data = json.dumps({"normalized": normalized, "attach_bracket_paste": attach_bracket_paste}) + + stdout = sys.stdout if sys.version_info < (3,) else sys.stdout.buffer + stdout.write(data.encode("utf-8")) + stdout.close() diff --git a/python_files/printEnvVariables.py b/python_files/printEnvVariables.py new file mode 100644 index 000000000000..bf2cfd80e666 --- /dev/null +++ b/python_files/printEnvVariables.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import os + +print(json.dumps(dict(os.environ))) diff --git a/python_files/printEnvVariablesToFile.py b/python_files/printEnvVariablesToFile.py new file mode 100644 index 000000000000..f6013a8c24cf --- /dev/null +++ b/python_files/printEnvVariablesToFile.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import sys + +# Prevent overwriting itself, since sys.argv[0] is the path to this file +if len(sys.argv) > 1: + # Last argument is the target file into which we'll write the env variables line by line. + output_file = sys.argv[-1] +else: + raise ValueError("Missing output file argument") + +with open(output_file, "w") as outfile: # noqa: PTH123 + for key, val in os.environ.items(): # noqa: FURB122 + outfile.write(f"{key}={val}\n") diff --git a/python_files/pyproject.toml b/python_files/pyproject.toml new file mode 100644 index 000000000000..7fb5e18339cb --- /dev/null +++ b/python_files/pyproject.toml @@ -0,0 +1,83 @@ +[tool.pyright] +exclude = ['lib'] +extraPaths = ['lib/python', 'lib/jedilsp'] +ignore = [ + # Ignore all pre-existing code with issues + 'get-pip.py', + 'tensorboard_launcher.py', + 'testlauncher.py', + 'visualstudio_py_testlauncher.py', + 'testing_tools/unittest_discovery.py', + 'testing_tools/adapter/util.py', + 'testing_tools/adapter/pytest/_discovery.py', + 'testing_tools/adapter/pytest/_pytest_item.py', + 'tests/testing_tools/adapter/.data', + 'tests/testing_tools/adapter/test___main__.py', + 'tests/testing_tools/adapter/test_discovery.py', + 'tests/testing_tools/adapter/test_functional.py', + 'tests/testing_tools/adapter/test_report.py', + 'tests/testing_tools/adapter/test_util.py', + 'tests/testing_tools/adapter/pytest/test_cli.py', + 'tests/testing_tools/adapter/pytest/test_discovery.py', +] + +[tool.ruff] +line-length = 100 +target-version = "py38" +exclude = [ + "**/.data", + "lib", +] + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +# Ruff's defaults are F and a subset of E. +# https://docs.astral.sh/ruff/rules/#rules +# Compatible w/ ruff formatter. https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules +# Up-to-date as of Ruff 0.5.0. +select = [ + "A", # flake8-builtins + "ARG", # flake8-unused-argument + "ASYNC", # flake8-async + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "D2", "D400", "D403", "D419", # pydocstyle + "DJ", # flake8-django + "DTZ", # flake8-dasetimez + "E4", "E7", "E9", # pycodestyle (errors) + "EXE", # flake8-executable + "F", # Pyflakes + "FBT", # flake8-boolean-trap + "FLY", # flynt + "FURB", # refurb + "I", # isort + "INP", # flake8-no-pep420 + "INT", # flake8-gettext + "LOG", # flake8-logging + "N", # pep8-naming + "NPY", # NumPy-specific rules + "PD", # pandas-vet + "PERF", # Perflint + "PIE", # flake8-pie + "PTH", # flake8-pathlib + # flake8-pytest-style + "PT006", "PT007", "PT009", "PT012", "PT014", "PT015", "PT016", "PT017", "PT018", "PT019", + "PT020", "PT021", "PT022", "PT024", "PT025", "PT026", "PT027", + "PYI", # flake8-pyi + "Q", # flake8-quotes + "RET502", "RET503", "RET504", # flake8-return + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "TCH", # flake8-type-checking + "UP", # pyupgrade + "W", # pycodestyle (warnings) + "YTT", # flake8-2020 +] + +[tool.ruff.lint.pydocstyle] +convention = "pep257" diff --git a/python_files/python_server.py b/python_files/python_server.py new file mode 100644 index 000000000000..e7ee92794a21 --- /dev/null +++ b/python_files/python_server.py @@ -0,0 +1,214 @@ +import ast +import contextlib +import io +import json +import sys +import traceback +import uuid +from pathlib import Path +from typing import Dict, List, Optional, Union + +STDIN = sys.stdin +STDOUT = sys.stdout +STDERR = sys.stderr +USER_GLOBALS = {} + + +def _send_message(msg: str): + # Content-Length is the data size in bytes. + length_msg = len(msg.encode()) + STDOUT.buffer.write(f"Content-Length: {length_msg}\r\n\r\n{msg}".encode()) + STDOUT.buffer.flush() + + +def send_message(**kwargs): + _send_message(json.dumps({"jsonrpc": "2.0", **kwargs})) + + +def print_log(msg: str): + send_message(method="log", params=msg) + + +def send_response( + response: str, + response_id: int, + execution_status: bool = True, # noqa: FBT001, FBT002 +): + send_message( + id=response_id, + result={"status": execution_status, "output": response}, + ) + + +def send_request(params: Optional[Union[List, Dict]] = None): + request_id = uuid.uuid4().hex + if params is None: + send_message(id=request_id, method="input") + else: + send_message(id=request_id, method="input", params=params) + + return request_id + + +original_input = input + + +def custom_input(prompt=""): + try: + send_request({"prompt": prompt}) + headers = get_headers() + # Content-Length is the data size in bytes. + content_length = int(headers.get("Content-Length", 0)) + + if content_length: + message_text = STDIN.buffer.read(content_length).decode() + message_json = json.loads(message_text) + return message_json["result"]["userInput"] + except EOFError: + # Input stream closed, exit gracefully + sys.exit(0) + except Exception: + print_log(traceback.format_exc()) + + +# Set input to our custom input +USER_GLOBALS["input"] = custom_input +input = custom_input # noqa: A001 + + +def handle_response(request_id): + while True: + try: + headers = get_headers() + # Content-Length is the data size in bytes. + content_length = int(headers.get("Content-Length", 0)) + + if content_length: + message_text = STDIN.buffer.read(content_length).decode() + message_json = json.loads(message_text) + our_user_input = message_json["result"]["userInput"] + if message_json["id"] == request_id: + send_response(our_user_input, message_json["id"]) + elif message_json["method"] == "exit": + sys.exit(0) + except EOFError: # noqa: PERF203 + # Input stream closed, exit gracefully + sys.exit(0) + except Exception: + print_log(traceback.format_exc()) + + +def exec_function(user_input): + try: + compile(user_input, "<stdin>", "eval") + except SyntaxError: + return exec + return eval + + +def check_valid_command(request): + try: + user_input = request["params"] + ast.parse(user_input[0]) + send_response("True", request["id"]) + except SyntaxError: + send_response("False", request["id"]) + + +def execute(request, user_globals): + str_output = CustomIO("<stdout>", encoding="utf-8") + str_error = CustomIO("<stderr>", encoding="utf-8") + str_input = CustomIO("<stdin>", encoding="utf-8", newline="\n") + + with contextlib.redirect_stdout(str_output), contextlib.redirect_stderr(str_error): + original_stdin = sys.stdin + try: + sys.stdin = str_input + execution_status = exec_user_input(request["params"], user_globals) + finally: + sys.stdin = original_stdin + + send_response(str_output.get_value(), request["id"], execution_status) + + +def exec_user_input(user_input, user_globals) -> bool: + user_input = user_input[0] if isinstance(user_input, list) else user_input + + try: + callable_ = exec_function(user_input) + retval = callable_(user_input, user_globals) + if retval is not None: + print(retval) + return True + except KeyboardInterrupt: + print(traceback.format_exc()) + return False + except Exception: + print(traceback.format_exc()) + return False + + +class CustomIO(io.TextIOWrapper): + """Custom stream object to replace stdio.""" + + def __init__(self, name, encoding="utf-8", newline=None): + self._buffer = io.BytesIO() + self._custom_name = name + super().__init__(self._buffer, encoding=encoding, newline=newline) + + def close(self): + """Provide this close method which is used by some tools.""" + # This is intentionally empty. + + def get_value(self) -> str: + """Returns value from the buffer as string.""" + self.seek(0) + return self.read() + + +def get_headers(): + headers = {} + while True: + raw = STDIN.buffer.readline() + # Detect EOF: readline() returns empty bytes when input stream is closed + if raw == b"": + raise EOFError("EOF reached while reading headers") + line = raw.decode().strip() + if not line: + break + name, value = line.split(":", 1) + headers[name] = value.strip() + return headers + + +if __name__ == "__main__": + # https://docs.python.org/3/tutorial/modules.html#the-module-search-path + # The directory containing the input script (or the current directory when no file is specified). + # Here we emulate the same behavior like no file is specified. + input_script_dir = Path(__file__).parent + script_dir_str = str(input_script_dir) + if script_dir_str in sys.path: + sys.path.remove(script_dir_str) + while "" in sys.path: + sys.path.remove("") + sys.path.insert(0, "") + while True: + try: + headers = get_headers() + # Content-Length is the data size in bytes. + content_length = int(headers.get("Content-Length", 0)) + + if content_length: + request_text = STDIN.buffer.read(content_length).decode() + request_json = json.loads(request_text) + if request_json["method"] == "execute": + execute(request_json, USER_GLOBALS) + if request_json["method"] == "check_valid_command": + check_valid_command(request_json) + elif request_json["method"] == "exit": + sys.exit(0) + except EOFError: # noqa: PERF203 + # Input stream closed (VS Code terminated), exit gracefully + sys.exit(0) + except Exception: + print_log(traceback.format_exc()) diff --git a/python_files/pythonrc.py b/python_files/pythonrc.py new file mode 100644 index 000000000000..3042ffb7a309 --- /dev/null +++ b/python_files/pythonrc.py @@ -0,0 +1,88 @@ +import platform +import sys + +if sys.platform != "win32": + import readline + +original_ps1 = ">>> " +is_wsl = "microsoft-standard-WSL" in platform.release() + + +class REPLHooks: + def __init__(self): + self.global_exit = None + self.failure_flag = False + self.original_excepthook = sys.excepthook + self.original_displayhook = sys.displayhook + sys.excepthook = self.my_excepthook + sys.displayhook = self.my_displayhook + + def my_displayhook(self, value): + if value is None: + self.failure_flag = False + + self.original_displayhook(value) + + def my_excepthook(self, type_, value, traceback): + self.global_exit = value + self.failure_flag = True + + self.original_excepthook(type_, value, traceback) + + +def get_last_command(): + # Get the last history item + last_command = "" + if sys.platform != "win32": + last_command = readline.get_history_item(readline.get_current_history_length()) + + return last_command + + +class PS1: + hooks = REPLHooks() + sys.excepthook = hooks.my_excepthook + sys.displayhook = hooks.my_displayhook + + # str will get called for every prompt with exit code to show success/failure + def __str__(self): + exit_code = int(bool(self.hooks.failure_flag)) + self.hooks.failure_flag = False + # Guide following official VS Code doc for shell integration sequence: + result = "" + # For non-windows allow recent_command history. + if sys.platform != "win32": + result = "{soh}{command_executed}{command_line}{command_finished}{prompt_started}{stx}{prompt}{soh}{command_start}{stx}".format( + soh="\001", + stx="\002", + command_executed="\x1b]633;C\x07", + command_line="\x1b]633;E;" + str(get_last_command()) + "\x07", + command_finished="\x1b]633;D;" + str(exit_code) + "\x07", + prompt_started="\x1b]633;A\x07", + prompt=original_ps1, + command_start="\x1b]633;B\x07", + ) + else: + result = "{command_finished}{prompt_started}{prompt}{command_start}{command_executed}".format( + command_finished="\x1b]633;D;" + str(exit_code) + "\x07", + prompt_started="\x1b]633;A\x07", + prompt=original_ps1, + command_start="\x1b]633;B\x07", + command_executed="\x1b]633;C\x07", + ) + + # result = f"{chr(27)}]633;D;{exit_code}{chr(7)}{chr(27)}]633;A{chr(7)}{original_ps1}{chr(27)}]633;B{chr(7)}{chr(27)}]633;C{chr(7)}" + + return result + + def __repr__(self): + return "<Custom PS1 for VS Code Python Shell Integration>" + + +if sys.platform != "win32" and (not is_wsl): + sys.ps1 = PS1() + +if sys.platform == "darwin": + print("Cmd click to launch VS Code Native REPL") +else: + print("Ctrl click to launch VS Code Native REPL") diff --git a/python_files/run-jedi-language-server.py b/python_files/run-jedi-language-server.py new file mode 100644 index 000000000000..47bf503d596c --- /dev/null +++ b/python_files/run-jedi-language-server.py @@ -0,0 +1,14 @@ +import os +import pathlib +import sys + +# Add the lib path to our sys path so jedi_language_server can find its references +extension_dir = pathlib.Path(__file__).parent.parent +EXTENSION_ROOT = os.fsdecode(extension_dir) +sys.path.insert(0, os.fsdecode(extension_dir / "python_files" / "lib" / "jedilsp")) +del extension_dir + + +from jedi_language_server.cli import cli # noqa: E402 + +sys.exit(cli()) diff --git a/python_files/shell_exec.py b/python_files/shell_exec.py new file mode 100644 index 000000000000..62b6b28af6cd --- /dev/null +++ b/python_files/shell_exec.py @@ -0,0 +1,42 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import subprocess +import sys + +# This is a simple solution to waiting for completion of commands sent to terminal. +# 1. Intercept commands send to a terminal +# 2. Send commands to our script file with an additional argument +# 3. In here create a file that'll log the progress. +# 4. Calling code monitors the contents of the file to determine state of execution. + +# Last argument is a file that's used for synchronizing the actions in the terminal with the calling code in extension. +lock_file = sys.argv[-1] +shell_args = sys.argv[1:-1] + +print("Executing command in shell >> " + " ".join(shell_args)) + +with open(lock_file, "w") as fp: # noqa: PTH123 + try: + # Signal start of execution. + fp.write("START\n") + fp.flush() + + subprocess.check_call(shell_args, stdout=sys.stdout, stderr=sys.stderr) + + # Signal start of execution. + fp.write("END\n") + fp.flush() + except Exception: + import traceback + + print(traceback.format_exc()) + # Signal end of execution with failure state. + fp.write("FAIL\n") + fp.flush() + try: + # ALso log the error for use from the other side. + with open(lock_file + ".error", "w") as fp_error: # noqa: PTH123 + fp_error.write(traceback.format_exc()) + except Exception: + pass diff --git a/python_files/tensorboard_launcher.py b/python_files/tensorboard_launcher.py new file mode 100644 index 000000000000..a04d51e7eb74 --- /dev/null +++ b/python_files/tensorboard_launcher.py @@ -0,0 +1,36 @@ +import contextlib +import mimetypes +import os +import sys +import time + +from tensorboard import program + + +def main(logdir): + # Environment variable for PyTorch profiler TensorBoard plugin + # to detect when it's running inside VS Code + os.environ["VSCODE_TENSORBOARD_LAUNCH"] = "1" + + # Work around incorrectly configured MIME types on Windows + mimetypes.add_type("application/javascript", ".js") + + # Start TensorBoard using their Python API + tb = program.TensorBoard() + tb.configure(bind_all=False, logdir=logdir) + url = tb.launch() + sys.stdout.write(f"TensorBoard started at {url}\n") + sys.stdout.flush() + + with contextlib.suppress(KeyboardInterrupt): + while True: + time.sleep(60) + sys.stdout.write("TensorBoard is shutting down") + sys.stdout.flush() + + +if __name__ == "__main__": + if len(sys.argv) == 2: + logdir = str(sys.argv[1]) + sys.stdout.write(f"Starting TensorBoard with logdir {logdir}") + main(logdir) diff --git a/tpn/tpn/__init__.py b/python_files/testing_tools/__init__.py similarity index 100% rename from tpn/tpn/__init__.py rename to python_files/testing_tools/__init__.py diff --git a/python_files/testing_tools/socket_manager.py b/python_files/testing_tools/socket_manager.py new file mode 100644 index 000000000000..f143ac111cdb --- /dev/null +++ b/python_files/testing_tools/socket_manager.py @@ -0,0 +1,95 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import contextlib +import socket +import sys + +# set the socket before it gets blocked or overwritten by a user tests +_SOCKET = socket.socket + + +class PipeManager: + def __init__(self, name): + self.name = name + + def __enter__(self): + return self.connect() + + def __exit__(self, *_): + self.close() + + def connect(self): + self._writer = open(self.name, "w", encoding="utf-8") # noqa: SIM115, PTH123 + # reader created in read method + return self + + def close(self): + self._writer.close() + if hasattr(self, "_reader"): + self._reader.close() + + def write(self, data: str): + try: + # for windows, is should only use \n\n + request = f"""content-length: {len(data)}\ncontent-type: application/json\n\n{data}""" + self._writer.write(request) + self._writer.flush() + except Exception as e: + print("error attempting to write to pipe", e) + raise (e) + + def read(self, bufsize=1024) -> str: + """Read data from the socket. + + Args: + bufsize (int): Number of bytes to read from the socket. + + Returns: + data (str): Data received from the socket. + """ + # returns a string automatically from read + if not hasattr(self, "_reader"): + self._reader = open(self.name, encoding="utf-8") # noqa: SIM115, PTH123 + return self._reader.read(bufsize) + + +class SocketManager: + """Create a socket and connect to the given address. + + The address is a (host: str, port: int) tuple. + Example usage: + + ``` + with SocketManager(("localhost", 6767)) as sock: + request = json.dumps(payload) + result = s.socket.sendall(request.encode("utf-8")) + ``` + """ + + def __init__(self, addr): + self.addr = addr + self.socket = None + + def __enter__(self): + return self.connect() + + def __exit__(self, *_): + self.close() + + def connect(self): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) + if sys.platform == "win32": + addr_use = socket.SO_EXCLUSIVEADDRUSE + else: + addr_use = socket.SO_REUSEADDR + self.socket.setsockopt(socket.SOL_SOCKET, addr_use, 1) + self.socket.connect(self.addr) + + return self + + def close(self): + if self.socket: + with contextlib.suppress(Exception): + self.socket.shutdown(socket.SHUT_RDWR) + self.socket.close() diff --git a/python_files/testlauncher.py b/python_files/testlauncher.py new file mode 100644 index 000000000000..2309a203363b --- /dev/null +++ b/python_files/testlauncher.py @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import sys + + +def parse_argv(): + """Parses arguments for use with the test launcher. + + Arguments are: + 1. Working directory. + 2. Test runner `pytest` + 3. Rest of the arguments are passed into the test runner. + """ + cwd = sys.argv[1] + test_runner = sys.argv[2] + args = sys.argv[3:] + + return (cwd, test_runner, args) + + +def run(cwd, test_runner, args): + """Runs the test. + + cwd -- the current directory to be set + testRunner -- test runner to be used `pytest` + args -- arguments passed into the test runner + """ + sys.path[0] = os.getcwd() # noqa: PTH109 + os.chdir(cwd) + + try: + if test_runner == "pytest": + import pytest + + pytest.main(args) + sys.exit(0) + finally: + pass + + +if __name__ == "__main__": + cwd, test_runner, args = parse_argv() + run(cwd, test_runner, args) diff --git a/python_files/tests/__init__.py b/python_files/tests/__init__.py new file mode 100644 index 000000000000..86bc29ff33e8 --- /dev/null +++ b/python_files/tests/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +# ruff:noqa: PTH118, PTH120 +import os.path + +TEST_ROOT = os.path.dirname(__file__) +SRC_ROOT = os.path.dirname(TEST_ROOT) +PROJECT_ROOT = os.path.dirname(SRC_ROOT) +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/python_files/tests/__main__.py b/python_files/tests/__main__.py new file mode 100644 index 000000000000..2595fce358e4 --- /dev/null +++ b/python_files/tests/__main__.py @@ -0,0 +1,55 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import sys + +import pytest + +from . import DEBUG_ADAPTER_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) + + if remainder: + for arg in remainder: + if arg.startswith("-") and arg not in ("-v", "--verbose", "-h", "--help"): + specific = False + break + else: + specific = True + else: + specific = False + args.specific = specific + + return ns, remainder + + +def main(pytestargs, markers=None, specific=False): # noqa: FBT002 + sys.path.insert(1, TESTING_TOOLS_ROOT) + sys.path.insert(1, DEBUG_ADAPTER_ROOT) + + if not specific: + pytestargs.insert(0, TEST_ROOT) + pytestargs.insert(0, "--rootdir") + pytestargs.insert(1, SRC_ROOT) + for marker in reversed(markers or ()): + pytestargs.insert(0, marker) + pytestargs.insert(0, "-m") + + return pytest.main(pytestargs) + + +if __name__ == "__main__": + mainkwargs, pytestargs = parse_args() + ec = main(pytestargs, **mainkwargs) + sys.exit(ec) diff --git a/python_files/tests/pytestadapter/.data/2496-black-formatter/app.py b/python_files/tests/pytestadapter/.data/2496-black-formatter/app.py new file mode 100644 index 000000000000..3b474e9d911e --- /dev/null +++ b/python_files/tests/pytestadapter/.data/2496-black-formatter/app.py @@ -0,0 +1,6 @@ +def add(a, b): + return a + b + + +def subtract(a, b): + return a - b diff --git a/python_files/tests/pytestadapter/.data/2496-black-formatter/test_app.py b/python_files/tests/pytestadapter/.data/2496-black-formatter/test_app.py new file mode 100644 index 000000000000..ef4398feb786 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/2496-black-formatter/test_app.py @@ -0,0 +1,14 @@ +import pytest +from app import add, subtract + + +def test_add(): # test_marker--test_add + assert add(2, 3) == 5 + assert add(-1, 1) == 0 + assert add(0, 0) == 0 + + +def test_subtract(): # test_marker--test_subtract + assert subtract(5, 3) == 2 + assert subtract(0, 0) == 0 + assert subtract(-1, -1) == 0 diff --git a/python_files/tests/pytestadapter/.data/config_sub_folder/config/pytest.ini b/python_files/tests/pytestadapter/.data/config_sub_folder/config/pytest.ini new file mode 100644 index 000000000000..dfac39a723e8 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/config_sub_folder/config/pytest.ini @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +[pytest] +python_files = + test_*.py +testpaths = + tests diff --git a/python_files/tests/pytestadapter/.data/config_sub_folder/tests/test_hello.py b/python_files/tests/pytestadapter/.data/config_sub_folder/tests/test_hello.py new file mode 100644 index 000000000000..2fd5e2b0a309 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/config_sub_folder/tests/test_hello.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def test_hello(): # test_marker--test_hello + assert True diff --git a/tpn/tpn/tests/__init__.py b/python_files/tests/pytestadapter/.data/coverage_gen/__init__.py similarity index 100% rename from tpn/tpn/tests/__init__.py rename to python_files/tests/pytestadapter/.data/coverage_gen/__init__.py diff --git a/python_files/tests/pytestadapter/.data/coverage_gen/reverse.py b/python_files/tests/pytestadapter/.data/coverage_gen/reverse.py new file mode 100644 index 000000000000..cb6755a3a369 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/coverage_gen/reverse.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +def reverse_string(s): + if s is None or s == "": + return "Error: Input is None" + return s[::-1] + +def reverse_sentence(sentence): + if sentence is None or sentence == "": + return "Error: Input is None" + words = sentence.split() + reversed_words = [reverse_string(word) for word in words] + return " ".join(reversed_words) + +# Example usage +if __name__ == "__main__": + sample_string = "hello" + print(reverse_string(sample_string)) # Output: "olleh" diff --git a/python_files/tests/pytestadapter/.data/coverage_gen/test_reverse.py b/python_files/tests/pytestadapter/.data/coverage_gen/test_reverse.py new file mode 100644 index 000000000000..e7319f143608 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/coverage_gen/test_reverse.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .reverse import reverse_sentence, reverse_string + + +def test_reverse_sentence(): + """ + Tests the reverse_sentence function to ensure it correctly reverses each word in a sentence. + + Test cases: + - "hello world" should be reversed to "olleh dlrow" + - "Python is fun" should be reversed to "nohtyP si nuf" + - "a b c" should remain "a b c" as each character is a single word + """ + assert reverse_sentence("hello world") == "olleh dlrow" + assert reverse_sentence("Python is fun") == "nohtyP si nuf" + assert reverse_sentence("a b c") == "a b c" + +def test_reverse_sentence_error(): + assert reverse_sentence("") == "Error: Input is None" + assert reverse_sentence(None) == "Error: Input is None" + + +def test_reverse_string(): + assert reverse_string("hello") == "olleh" + assert reverse_string("Python") == "nohtyP" + # this test specifically does not cover the error cases diff --git a/python_files/tests/pytestadapter/.data/coverage_w_config/pyproject.toml b/python_files/tests/pytestadapter/.data/coverage_w_config/pyproject.toml new file mode 100644 index 000000000000..c3406cc68929 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/coverage_w_config/pyproject.toml @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +[tool.coverage.report] +omit = ["test_ignore.py", "tests/*.py"] diff --git a/python_files/tests/pytestadapter/.data/coverage_w_config/test_ignore.py b/python_files/tests/pytestadapter/.data/coverage_w_config/test_ignore.py new file mode 100644 index 000000000000..98640e336ab4 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/coverage_w_config/test_ignore.py @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +def test_to_ignore(): + assert True diff --git a/python_files/tests/pytestadapter/.data/coverage_w_config/test_ran.py b/python_files/tests/pytestadapter/.data/coverage_w_config/test_ran.py new file mode 100644 index 000000000000..864acec79ba2 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/coverage_w_config/test_ran.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +def test_simple(): + assert True + + +def untouched_function(): + return 1 diff --git a/python_files/tests/pytestadapter/.data/coverage_w_config/tests/test_disregard.py b/python_files/tests/pytestadapter/.data/coverage_w_config/tests/test_disregard.py new file mode 100644 index 000000000000..110a11534171 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/coverage_w_config/tests/test_disregard.py @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +def test_i_hope_this_is_ignored(): + assert True diff --git a/python_files/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py b/python_files/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py new file mode 100644 index 000000000000..59738aeba37f --- /dev/null +++ b/python_files/tests/pytestadapter/.data/dual_level_nested_folder/nested_folder_one/test_bottom_folder.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +# This test's id is dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t. +# This test passes. +def test_bottom_function_t(): # test_marker--test_bottom_function_t + assert True + + +# This test's id is dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f. +# This test fails. +def test_bottom_function_f(): # test_marker--test_bottom_function_f + assert False diff --git a/python_files/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py b/python_files/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py new file mode 100644 index 000000000000..010c54cf4461 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/dual_level_nested_folder/test_top_folder.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +# This test's id is dual_level_nested_folder/test_top_folder.py::test_top_function_t. +# This test passes. +def test_top_function_t(): # test_marker--test_top_function_t + assert True + + +# This test's id is dual_level_nested_folder/test_top_folder.py::test_top_function_f. +# This test fails. +def test_top_function_f(): # test_marker--test_top_function_f + assert False diff --git a/python_files/tests/pytestadapter/.data/empty_discovery.py b/python_files/tests/pytestadapter/.data/empty_discovery.py new file mode 100644 index 000000000000..5f4ea27aec7f --- /dev/null +++ b/python_files/tests/pytestadapter/.data/empty_discovery.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +# This file has no tests in it; the discovery will return an empty list of tests. +def function_function(string): + return string diff --git a/python_files/tests/pytestadapter/.data/error_parametrize_discovery.py b/python_files/tests/pytestadapter/.data/error_parametrize_discovery.py new file mode 100644 index 000000000000..8e48224edf3b --- /dev/null +++ b/python_files/tests/pytestadapter/.data/error_parametrize_discovery.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest + + +# This test has an error which will appear on pytest discovery. +# This error is intentional and is meant to test pytest discovery error handling. +@pytest.mark.parametrize("actual,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)]) +def test_function(): + assert True diff --git a/python_files/tests/pytestadapter/.data/error_pytest_import.txt b/python_files/tests/pytestadapter/.data/error_pytest_import.txt new file mode 100644 index 000000000000..7d65dee2ccc6 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/error_pytest_import.txt @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +@pytest.mark.parametrize("num", range(1, 89)) +def test_odd_even(num): + assert True diff --git a/python_files/tests/pytestadapter/.data/error_raise_exception.py b/python_files/tests/pytestadapter/.data/error_raise_exception.py new file mode 100644 index 000000000000..2506089abe07 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/error_raise_exception.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + + +@pytest.fixture +def raise_fixture(): + raise Exception("Dummy exception") + + +class TestSomething: + def test_a(self, raise_fixture): + assert True diff --git a/python_files/tests/pytestadapter/.data/error_syntax_discovery.txt b/python_files/tests/pytestadapter/.data/error_syntax_discovery.txt new file mode 100644 index 000000000000..78627fffb351 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/error_syntax_discovery.txt @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# This test has a syntax error. +# This error is intentional and is meant to test pytest discovery error handling. +def test_function() + assert True diff --git a/python_files/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py b/python_files/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py new file mode 100644 index 000000000000..9ac9f7017f87 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/folder_a/folder_b/folder_a/test_nest.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +# This test's id is double_nested_folder/nested_folder_one/nested_folder_two/test_nest.py::test_function. +# This test passes. +def test_function(): # test_marker--test_function + assert 1 == 1 diff --git a/python_files/tests/pytestadapter/.data/folder_with_script/script_random.py b/python_files/tests/pytestadapter/.data/folder_with_script/script_random.py new file mode 100644 index 000000000000..d8c32027a9e6 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/folder_with_script/script_random.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# This file has no test, it's just a random script. + +if __name__ == "__main__": + print("Hello World!") diff --git a/python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py b/python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py new file mode 100644 index 000000000000..9f9bfb014f3d --- /dev/null +++ b/python_files/tests/pytestadapter/.data/folder_with_script/test_simple.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +# This test passes. +def test_function(): # test_marker--test_function + assert 1 == 1 diff --git a/python_files/tests/pytestadapter/.data/param_same_name/test_param1.py b/python_files/tests/pytestadapter/.data/param_same_name/test_param1.py new file mode 100644 index 000000000000..a16d0f49f411 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/param_same_name/test_param1.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest + + +@pytest.mark.parametrize("num", ["a", "b", "c"]) +def test_odd_even(num): + assert True diff --git a/python_files/tests/pytestadapter/.data/param_same_name/test_param2.py b/python_files/tests/pytestadapter/.data/param_same_name/test_param2.py new file mode 100644 index 000000000000..c0ea8010e359 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/param_same_name/test_param2.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest + + +@pytest.mark.parametrize("num", range(1, 4)) +def test_odd_even(num): + assert True diff --git a/python_files/tests/pytestadapter/.data/parametrize_tests.py b/python_files/tests/pytestadapter/.data/parametrize_tests.py new file mode 100644 index 000000000000..34d3c4201f0f --- /dev/null +++ b/python_files/tests/pytestadapter/.data/parametrize_tests.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + + +class TestClass: + # Testing pytest with parametrized tests. The first two pass, the third fails. + # The tests ids are parametrize_tests.py::test_adding[3+5-8] and so on. + @pytest.mark.parametrize( # test_marker--test_adding + "actual, expected", [("3+5", 8), ("2+4", 6), ("6+9", 16)] + ) + def test_adding(self, actual, expected): + assert eval(actual) == expected + + +# Testing pytest with parametrized tests. All three pass. +# The tests ids are parametrize_tests.py::test_under_ten[1] and so on. +@pytest.mark.parametrize( # test_marker--test_string + "string", ["hello", "complicated split [] ()"] +) +def test_string(string): + assert string == "hello" diff --git a/python_files/tests/pytestadapter/.data/pytest.ini b/python_files/tests/pytestadapter/.data/pytest.ini new file mode 100644 index 000000000000..ddbcd6544e5d --- /dev/null +++ b/python_files/tests/pytestadapter/.data/pytest.ini @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# pytest.ini is specified here so the root directory of the tests is kept at .data instead of referencing +# the parent python_files/pyproject.toml for test_discovery.py and test_execution.py for pytest-adapter tests. diff --git a/python_files/tests/pytestadapter/.data/pytest_describe_plugin/describe_only.py b/python_files/tests/pytestadapter/.data/pytest_describe_plugin/describe_only.py new file mode 100644 index 000000000000..0702c032684b --- /dev/null +++ b/python_files/tests/pytestadapter/.data/pytest_describe_plugin/describe_only.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +def describe_A(): + def test_1(): # test_marker--test_1 + pass + + def test_2(): # test_marker--test_2 + pass diff --git a/python_files/tests/pytestadapter/.data/pytest_describe_plugin/nested_describe.py b/python_files/tests/pytestadapter/.data/pytest_describe_plugin/nested_describe.py new file mode 100644 index 000000000000..5b9c13cc8d53 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/pytest_describe_plugin/nested_describe.py @@ -0,0 +1,31 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + + +def describe_list(): + @pytest.fixture + def list(): + return [] + + def describe_append(): + def add_empty(list): # test_marker--add_empty + list.append("foo") + list.append("bar") + assert list == ["foo", "bar"] + + def remove_empty(list): # test_marker--remove_empty + try: + list.remove("foo") + except ValueError: + pass + + def describe_remove(): + @pytest.fixture + def list(): + return ["foo", "bar"] + + def removes(list): # test_marker--removes + list.remove("foo") + assert list == ["bar"] diff --git a/src/test/pythonFiles/autoimport/one.py b/python_files/tests/pytestadapter/.data/root/tests/pytest.ini similarity index 100% rename from src/test/pythonFiles/autoimport/one.py rename to python_files/tests/pytestadapter/.data/root/tests/pytest.ini diff --git a/python_files/tests/pytestadapter/.data/root/tests/test_a.py b/python_files/tests/pytestadapter/.data/root/tests/test_a.py new file mode 100644 index 000000000000..3ec3dd9626cb --- /dev/null +++ b/python_files/tests/pytestadapter/.data/root/tests/test_a.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def test_a_function(): # test_marker--test_a_function + assert True diff --git a/python_files/tests/pytestadapter/.data/root/tests/test_b.py b/python_files/tests/pytestadapter/.data/root/tests/test_b.py new file mode 100644 index 000000000000..0d3148641f85 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/root/tests/test_b.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +def test_b_function(): # test_marker--test_b_function + assert True diff --git a/python_files/tests/pytestadapter/.data/same_function_new_class_param.py b/python_files/tests/pytestadapter/.data/same_function_new_class_param.py new file mode 100644 index 000000000000..6f85051436b8 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/same_function_new_class_param.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import pytest + + +class TestNotEmpty: + @pytest.mark.parametrize("a, b", [(1, 1), (2, 2)]) # test_marker--TestNotEmpty::test_integer + def test_integer(self, a, b): + assert a == b + + @pytest.mark.parametrize( # test_marker--TestNotEmpty::test_string + "a, b", [("a", "a"), ("b", "b")] + ) + def test_string(self, a, b): + assert a == b + + +class TestEmpty: + @pytest.mark.parametrize("a, b", [(0, 0)]) # test_marker--TestEmpty::test_integer + def test_integer(self, a, b): + assert a == b + + @pytest.mark.parametrize("a, b", [("", "")]) # test_marker--TestEmpty::test_string + def test_string(self, a, b): + assert a == b diff --git a/python_files/tests/pytestadapter/.data/simple_pytest.py b/python_files/tests/pytestadapter/.data/simple_pytest.py new file mode 100644 index 000000000000..9f9bfb014f3d --- /dev/null +++ b/python_files/tests/pytestadapter/.data/simple_pytest.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +# This test passes. +def test_function(): # test_marker--test_function + assert 1 == 1 diff --git a/python_files/tests/pytestadapter/.data/skip_test_fixture.py b/python_files/tests/pytestadapter/.data/skip_test_fixture.py new file mode 100644 index 000000000000..3d354cae86ea --- /dev/null +++ b/python_files/tests/pytestadapter/.data/skip_test_fixture.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + + +@pytest.fixture +def docker_client() -> object: + try: + # NOTE: Actually connect with the docker sdk + raise Exception("Docker client not available") + except Exception: + pytest.skip("Docker client not available") + + return object() + + +def test_docker_client(docker_client): + assert False diff --git a/python_files/tests/pytestadapter/.data/skip_tests.py b/python_files/tests/pytestadapter/.data/skip_tests.py new file mode 100644 index 000000000000..871b0e7bf5c3 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/skip_tests.py @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import pytest + +# Testing pytest with skipped tests. The first passes, the second three are skipped. + + +def test_something(): # test_marker--test_something + # This tests passes successfully. + assert 1 + 1 == 2 + + +def test_another_thing(): # test_marker--test_another_thing + # Skip this test with a reason. + pytest.skip("Skipping this test for now") + + +@pytest.mark.skip( + reason="Skipping this test as it requires additional setup" # test_marker--test_complex_thing +) +def test_decorator_thing(): + # Skip this test as well, with a reason. This one uses a decorator. + assert True + + +@pytest.mark.skipif(1 < 5, reason="is always true") # test_marker--test_complex_thing_2 +def test_decorator_thing_2(): + # Skip this test as well, with a reason. This one uses a decorator with a condition. + assert True + + +# With this test, the entire class is skipped. +@pytest.mark.skip(reason="Skip TestClass") +class TestClass: + def test_class_function_a(self): # test_marker--test_class_function_a + assert True + + def test_class_function_b(self): # test_marker--test_class_function_b + assert False diff --git a/python_files/tests/pytestadapter/.data/test_env_vars.py b/python_files/tests/pytestadapter/.data/test_env_vars.py new file mode 100644 index 000000000000..c8a3add56763 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/test_env_vars.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os + + +def test_clear_env(monkeypatch): + # Clear all environment variables + monkeypatch.setattr(os, "environ", {}) + + # Now os.environ should be empty + assert not os.environ + + # After the test finishes, the environment variables will be reset to their original state + + +def test_check_env(): + # This test will have access to the original environment variables + assert "PATH" in os.environ + + +def test_clear_env_unsafe(): + # Clear all environment variables + os.environ.clear() + # Now os.environ should be empty + assert not os.environ + + +def test_check_env_unsafe(): + # ("PATH" in os.environ) is False here if it runs after test_clear_env_unsafe. + # Regardless, this test will pass and TEST_PORT and TEST_UUID will still be set correctly + assert "PATH" not in os.environ diff --git a/python_files/tests/pytestadapter/.data/test_logging.py b/python_files/tests/pytestadapter/.data/test_logging.py new file mode 100644 index 000000000000..058ad8075718 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/test_logging.py @@ -0,0 +1,35 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging +import sys + + +def test_logging2(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.") + + # Printing to stdout and stderr + print("This is a stdout message.") + print("This is a stderr message.", file=sys.stderr) + assert False + + +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.") + + # Printing to stdout and stderr + print("This is a stdout message.") + print("This is a stderr message.", file=sys.stderr) diff --git a/python_files/tests/pytestadapter/.data/test_multi_class_nest.py b/python_files/tests/pytestadapter/.data/test_multi_class_nest.py new file mode 100644 index 000000000000..209f9d51915b --- /dev/null +++ b/python_files/tests/pytestadapter/.data/test_multi_class_nest.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +class TestFirstClass: + class TestSecondClass: + def test_second(self): # test_marker--test_second + assert 1 == 2 + + def test_first(self): # test_marker--test_first + assert 1 == 2 + + class TestSecondClass2: + def test_second2(self): # test_marker--test_second2 + assert 1 == 1 + + +def test_independent(): # test_marker--test_independent + assert 1 == 1 diff --git a/python_files/tests/pytestadapter/.data/test_param_span_class.py b/python_files/tests/pytestadapter/.data/test_param_span_class.py new file mode 100644 index 000000000000..a024c438bbf9 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/test_param_span_class.py @@ -0,0 +1,16 @@ +import pytest + + +@pytest.fixture(scope="function", params=[1, 2]) +def setup(request): + return request.param + + +class TestClass1: + def test_method1(self, setup): # test_marker--TestClass1::test_method1 + assert 1 == 1 + + +class TestClass2: + def test_method1(self, setup): # test_marker--TestClass2::test_method1 + assert 2 == 2 diff --git a/python_files/tests/pytestadapter/.data/text_docstring.txt b/python_files/tests/pytestadapter/.data/text_docstring.txt new file mode 100644 index 000000000000..b29132c10b57 --- /dev/null +++ b/python_files/tests/pytestadapter/.data/text_docstring.txt @@ -0,0 +1,4 @@ +This is a doctest test which passes #test_marker--text_docstring.txt +>>> x = 3 +>>> x +3 diff --git a/python_files/tests/pytestadapter/.data/unittest_folder/test_add.py b/python_files/tests/pytestadapter/.data/unittest_folder/test_add.py new file mode 100644 index 000000000000..e9bdda0ad2ad --- /dev/null +++ b/python_files/tests/pytestadapter/.data/unittest_folder/test_add.py @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + + +def add(a, b): + return a + b + + +class TestAddFunction(unittest.TestCase): + # This test's id is unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers. + # This test passes. + def test_add_positive_numbers(self): # test_marker--test_add_positive_numbers + result = add(2, 3) + self.assertEqual(result, 5) + + # This test's id is unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers. + # This test passes. + def test_add_negative_numbers(self): # test_marker--test_add_negative_numbers + result = add(-2, -3) + self.assertEqual(result, -5) + + +class TestDuplicateFunction(unittest.TestCase): + # This test's id is unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_a. It has the same class name as + # another test, but it's in a different file, so it should not be confused. + # This test passes. + def test_dup_a(self): # test_marker--test_dup_a + self.assertEqual(1, 1) diff --git a/python_files/tests/pytestadapter/.data/unittest_folder/test_subtract.py b/python_files/tests/pytestadapter/.data/unittest_folder/test_subtract.py new file mode 100644 index 000000000000..634a6d81f9eb --- /dev/null +++ b/python_files/tests/pytestadapter/.data/unittest_folder/test_subtract.py @@ -0,0 +1,34 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + + +def subtract(a, b): + return a - b + + +class TestSubtractFunction(unittest.TestCase): + # This test's id is unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers. + # This test passes. + def test_subtract_positive_numbers( # test_marker--test_subtract_positive_numbers + self, + ): + result = subtract(5, 3) + self.assertEqual(result, 2) + + # This test's id is unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers. + # This test passes. + def test_subtract_negative_numbers( # test_marker--test_subtract_negative_numbers + self, + ): + result = subtract(-2, -3) + # This is intentional to test assertion failures + self.assertEqual(result, 100000) + + +class TestDuplicateFunction(unittest.TestCase): + # This test's id is unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s. It has the same class name as + # another test, but it's in a different file, so it should not be confused. + # This test passes. + def test_dup_s(self): # test_marker--test_dup_s + self.assertEqual(1, 1) diff --git a/python_files/tests/pytestadapter/.data/unittest_pytest_same_file.py b/python_files/tests/pytestadapter/.data/unittest_pytest_same_file.py new file mode 100644 index 000000000000..ac66779b9cbe --- /dev/null +++ b/python_files/tests/pytestadapter/.data/unittest_pytest_same_file.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + + +class TestExample(unittest.TestCase): + # This test's id is unittest_pytest_same_file.py::TestExample::test_true_unittest. + # Test type is unittest and this test passes. + def test_true_unittest(self): # test_marker--test_true_unittest + assert True + + +# This test's id is unittest_pytest_same_file.py::test_true_pytest. +# Test type is pytest and this test passes. +def test_true_pytest(): # test_marker--test_true_pytest + assert True diff --git a/python_files/tests/pytestadapter/.data/unittest_skiptest_file_level.py b/python_files/tests/pytestadapter/.data/unittest_skiptest_file_level.py new file mode 100644 index 000000000000..362c74cbb76f --- /dev/null +++ b/python_files/tests/pytestadapter/.data/unittest_skiptest_file_level.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +from unittest import SkipTest + +# Due to the skip at the file level, no tests will be discovered. +raise SkipTest("Skip all tests in this file, they should not be recognized by pytest.") + + +class SimpleTest(unittest.TestCase): + def testadd1(self): + assert True diff --git a/python_files/tests/pytestadapter/__init__.py b/python_files/tests/pytestadapter/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/pytestadapter/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/pytestadapter/expected_discovery_test_output.py b/python_files/tests/pytestadapter/expected_discovery_test_output.py new file mode 100644 index 000000000000..047f1c72ad17 --- /dev/null +++ b/python_files/tests/pytestadapter/expected_discovery_test_output.py @@ -0,0 +1,2083 @@ +import os + +from .helpers import ( + TEST_DATA_PATH, + find_class_line_number, + find_test_line_number, + get_absolute_test_id, +) + +# This file contains the expected output dictionaries for tests discovery and is used in test_discovery.py. + +# This is the expected output for the empty_discovery.py file. +# └── +TEST_DATA_PATH_STR = os.fspath(TEST_DATA_PATH) +empty_discovery_pytest_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the simple_pytest.py file. +# └── simple_pytest.py +# └── test_function +simple_test_file_path = TEST_DATA_PATH / "simple_pytest.py" +simple_discovery_pytest_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "simple_pytest.py", + "path": os.fspath(simple_test_file_path), + "type_": "file", + "id_": os.fspath(simple_test_file_path), + "children": [ + { + "name": "test_function", + "path": os.fspath(simple_test_file_path), + "lineno": find_test_line_number( + "test_function", + simple_test_file_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "simple_pytest.py::test_function", simple_test_file_path + ), + "runID": get_absolute_test_id( + "simple_pytest.py::test_function", simple_test_file_path + ), + } + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the unittest_pytest_same_file.py file. +# ├── unittest_pytest_same_file.py +# ├── TestExample +# │ └── test_true_unittest +# └── test_true_pytest +unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" +unit_pytest_same_file_discovery_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "unittest_pytest_same_file.py", + "path": os.fspath(unit_pytest_same_file_path), + "type_": "file", + "id_": os.fspath(unit_pytest_same_file_path), + "children": [ + { + "name": "TestExample", + "path": os.fspath(unit_pytest_same_file_path), + "type_": "class", + "children": [ + { + "name": "test_true_unittest", + "path": os.fspath(unit_pytest_same_file_path), + "lineno": find_test_line_number( + "test_true_unittest", + os.fspath(unit_pytest_same_file_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), + "runID": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), + } + ], + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample", + unit_pytest_same_file_path, + ), + "lineno": find_class_line_number("TestExample", unit_pytest_same_file_path), + }, + { + "name": "test_true_pytest", + "path": os.fspath(unit_pytest_same_file_path), + "lineno": find_test_line_number( + "test_true_pytest", + unit_pytest_same_file_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), + "runID": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the unittest_skip_file_level test. +# └── unittest_skiptest_file_level.py +unittest_skip_file_level_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the unittest_folder tests +# └── unittest_folder +# ├── test_add.py +# │ └── TestAddFunction +# │ ├── test_add_negative_numbers +# │ └── test_add_positive_numbers +# │ └── TestDuplicateFunction +# │ └── test_dup_a +# └── test_subtract.py +# └── TestSubtractFunction +# ├── test_subtract_negative_numbers +# └── test_subtract_positive_numbers +# │ └── TestDuplicateFunction +# │ └── test_dup_s +unittest_folder_path = TEST_DATA_PATH / "unittest_folder" +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" +unittest_folder_discovery_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "unittest_folder", + "path": os.fspath(unittest_folder_path), + "type_": "folder", + "id_": os.fspath(unittest_folder_path), + "children": [ + { + "name": "test_add.py", + "path": os.fspath(test_add_path), + "type_": "file", + "id_": os.fspath(test_add_path), + "children": [ + { + "name": "TestAddFunction", + "path": os.fspath(test_add_path), + "type_": "class", + "children": [ + { + "name": "test_add_negative_numbers", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_add_negative_numbers", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), + }, + { + "name": "test_add_positive_numbers", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_add_positive_numbers", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), + }, + ], + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestAddFunction", + test_add_path, + ), + "lineno": find_class_line_number("TestAddFunction", test_add_path), + }, + { + "name": "TestDuplicateFunction", + "path": os.fspath(test_add_path), + "type_": "class", + "children": [ + { + "name": "test_dup_a", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_dup_a", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), + }, + ], + "id_": get_absolute_test_id( + "unittest_folder/test_add.py::TestDuplicateFunction", + test_add_path, + ), + "lineno": find_class_line_number( + "TestDuplicateFunction", test_add_path + ), + }, + ], + }, + { + "name": "test_subtract.py", + "path": os.fspath(test_subtract_path), + "type_": "file", + "id_": os.fspath(test_subtract_path), + "children": [ + { + "name": "TestSubtractFunction", + "path": os.fspath(test_subtract_path), + "type_": "class", + "children": [ + { + "name": "test_subtract_negative_numbers", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_subtract_negative_numbers", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), + }, + { + "name": "test_subtract_positive_numbers", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_subtract_positive_numbers", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), + }, + ], + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestSubtractFunction", + test_subtract_path, + ), + "lineno": find_class_line_number( + "TestSubtractFunction", test_subtract_path + ), + }, + { + "name": "TestDuplicateFunction", + "path": os.fspath(test_subtract_path), + "type_": "class", + "children": [ + { + "name": "test_dup_s", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_dup_s", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), + }, + ], + "id_": get_absolute_test_id( + "unittest_folder/test_subtract.py::TestDuplicateFunction", + test_subtract_path, + ), + "lineno": find_class_line_number( + "TestDuplicateFunction", test_subtract_path + ), + }, + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + + +# This is the expected output for the dual_level_nested_folder tests +# └── dual_level_nested_folder +# └── test_top_folder.py +# └── test_top_function_t +# └── test_top_function_f +# └── nested_folder_one +# └── test_bottom_folder.py +# └── test_bottom_function_t +# └── test_bottom_function_f +dual_level_nested_folder_path = TEST_DATA_PATH / "dual_level_nested_folder" +test_top_folder_path = TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" + +test_nested_folder_one_path = TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" + +test_bottom_folder_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" +) + + +dual_level_nested_folder_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "dual_level_nested_folder", + "path": os.fspath(dual_level_nested_folder_path), + "type_": "folder", + "id_": os.fspath(dual_level_nested_folder_path), + "children": [ + { + "name": "test_top_folder.py", + "path": os.fspath(test_top_folder_path), + "type_": "file", + "id_": os.fspath(test_top_folder_path), + "children": [ + { + "name": "test_top_function_t", + "path": os.fspath(test_top_folder_path), + "lineno": find_test_line_number( + "test_top_function_t", + test_top_folder_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + test_top_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + test_top_folder_path, + ), + }, + { + "name": "test_top_function_f", + "path": os.fspath(test_top_folder_path), + "lineno": find_test_line_number( + "test_top_function_f", + test_top_folder_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + test_top_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + test_top_folder_path, + ), + }, + ], + }, + { + "name": "nested_folder_one", + "path": os.fspath(test_nested_folder_one_path), + "type_": "folder", + "id_": os.fspath(test_nested_folder_one_path), + "children": [ + { + "name": "test_bottom_folder.py", + "path": os.fspath(test_bottom_folder_path), + "type_": "file", + "id_": os.fspath(test_bottom_folder_path), + "children": [ + { + "name": "test_bottom_function_t", + "path": os.fspath(test_bottom_folder_path), + "lineno": find_test_line_number( + "test_bottom_function_t", + test_bottom_folder_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + test_bottom_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + test_bottom_folder_path, + ), + }, + { + "name": "test_bottom_function_f", + "path": os.fspath(test_bottom_folder_path), + "lineno": find_test_line_number( + "test_bottom_function_f", + test_bottom_folder_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + test_bottom_folder_path, + ), + "runID": get_absolute_test_id( + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + test_bottom_folder_path, + ), + }, + ], + } + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the double_nested_folder tests. +# └── folder_a +# └── folder_b +# └── folder_a +# └── test_nest.py +# └── test_function + +folder_a_path = TEST_DATA_PATH / "folder_a" +folder_b_path = TEST_DATA_PATH / "folder_a" / "folder_b" +folder_a_nested_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" +test_nest_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +double_nested_folder_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "folder_a", + "path": os.fspath(folder_a_path), + "type_": "folder", + "id_": os.fspath(folder_a_path), + "children": [ + { + "name": "folder_b", + "path": os.fspath(folder_b_path), + "type_": "folder", + "id_": os.fspath(folder_b_path), + "children": [ + { + "name": "folder_a", + "path": os.fspath(folder_a_nested_path), + "type_": "folder", + "id_": os.fspath(folder_a_nested_path), + "children": [ + { + "name": "test_nest.py", + "path": os.fspath(test_nest_path), + "type_": "file", + "id_": os.fspath(test_nest_path), + "children": [ + { + "name": "test_function", + "path": os.fspath(test_nest_path), + "lineno": find_test_line_number( + "test_function", + test_nest_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", + test_nest_path, + ), + "runID": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", + test_nest_path, + ), + } + ], + } + ], + } + ], + } + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the nested_folder tests. +# └── parametrize_tests.py +# └── TestClass +# └── test_adding +# └── [3+5-8] +# └── [2+4-6] +# └── [6+9-16] +# └── test_string +# └── [hello] +# └── [complicated split [] ()] +parameterize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" +parametrize_tests_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "parametrize_tests.py", + "path": os.fspath(parameterize_tests_path), + "type_": "file", + "id_": os.fspath(parameterize_tests_path), + "children": [ + { + "name": "TestClass", + "path": os.fspath(parameterize_tests_path), + "type_": "class", + "id_": get_absolute_test_id( + "parametrize_tests.py::TestClass", + parameterize_tests_path, + ), + "lineno": find_class_line_number("TestClass", parameterize_tests_path), + "children": [ + { + "name": "test_adding", + "path": os.fspath(parameterize_tests_path), + "type_": "function", + "id_": os.fspath(parameterize_tests_path) + "::TestClass::test_adding", + "children": [ + { + "name": "[3+5-8]", + "path": os.fspath(parameterize_tests_path), + "lineno": find_test_line_number( + "test_adding[3+5-8]", + parameterize_tests_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[3+5-8]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[3+5-8]", + parameterize_tests_path, + ), + }, + { + "name": "[2+4-6]", + "path": os.fspath(parameterize_tests_path), + "lineno": find_test_line_number( + "test_adding[2+4-6]", + parameterize_tests_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[2+4-6]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[2+4-6]", + parameterize_tests_path, + ), + }, + { + "name": "[6+9-16]", + "path": os.fspath(parameterize_tests_path), + "lineno": find_test_line_number( + "test_adding[6+9-16]", + parameterize_tests_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[6+9-16]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[6+9-16]", + parameterize_tests_path, + ), + }, + ], + }, + ], + }, + { + "name": "test_string", + "path": os.fspath(parameterize_tests_path), + "type_": "function", + "children": [ + { + "name": "[hello]", + "path": os.fspath(parameterize_tests_path), + "lineno": find_test_line_number( + "test_string[hello]", + parameterize_tests_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_string[hello]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_string[hello]", + parameterize_tests_path, + ), + }, + { + "name": "[complicated split [] ()]", + "path": os.fspath(parameterize_tests_path), + "lineno": find_test_line_number( + "test_string[1]", + parameterize_tests_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "parametrize_tests.py::test_string[complicated split [] ()]", + parameterize_tests_path, + ), + "runID": get_absolute_test_id( + "parametrize_tests.py::test_string[complicated split [] ()]", + parameterize_tests_path, + ), + }, + ], + "id_": os.fspath(parameterize_tests_path) + "::test_string", + }, + ], + }, + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the text_docstring.txt tests. +# └── text_docstring.txt +text_docstring_path = TEST_DATA_PATH / "text_docstring.txt" +doctest_pytest_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "text_docstring.txt", + "path": os.fspath(text_docstring_path), + "type_": "file", + "id_": os.fspath(text_docstring_path), + "children": [ + { + "name": "text_docstring.txt", + "path": os.fspath(text_docstring_path), + "lineno": find_test_line_number( + "text_docstring.txt", + os.fspath(text_docstring_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", text_docstring_path + ), + "runID": get_absolute_test_id( + "text_docstring.txt::text_docstring.txt", text_docstring_path + ), + } + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the param_same_name tests. +# └── param_same_name +# └── test_param1.py +# └── test_odd_even +# └── [a] +# └── [b] +# └── [c] +# └── test_param2.py +# └── test_odd_even +# └── [1] +# └── [2] +# └── [3] +param1_path = TEST_DATA_PATH / "param_same_name" / "test_param1.py" +param2_path = TEST_DATA_PATH / "param_same_name" / "test_param2.py" +param_same_name_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "param_same_name", + "path": os.fspath(TEST_DATA_PATH / "param_same_name"), + "type_": "folder", + "id_": os.fspath(TEST_DATA_PATH / "param_same_name"), + "children": [ + { + "name": "test_param1.py", + "path": os.fspath(param1_path), + "type_": "file", + "id_": os.fspath(param1_path), + "children": [ + { + "name": "test_odd_even", + "path": os.fspath(param1_path), + "type_": "function", + "children": [ + { + "name": "[a]", + "path": os.fspath(param1_path), + "lineno": "6", + "type_": "test", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[a]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[a]", + param1_path, + ), + }, + { + "name": "[b]", + "path": os.fspath(param1_path), + "lineno": "6", + "type_": "test", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[b]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[b]", + param1_path, + ), + }, + { + "name": "[c]", + "path": os.fspath(param1_path), + "lineno": "6", + "type_": "test", + "id_": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[c]", + param1_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param1.py::test_odd_even[c]", + param1_path, + ), + }, + ], + "id_": os.fspath(param1_path) + "::test_odd_even", + } + ], + }, + { + "name": "test_param2.py", + "path": os.fspath(param2_path), + "type_": "file", + "id_": os.fspath(param2_path), + "children": [ + { + "name": "test_odd_even", + "path": os.fspath(param2_path), + "type_": "function", + "children": [ + { + "name": "[1]", + "path": os.fspath(param2_path), + "lineno": "6", + "type_": "test", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[1]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[1]", + param2_path, + ), + }, + { + "name": "[2]", + "path": os.fspath(param2_path), + "lineno": "6", + "type_": "test", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[2]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[2]", + param2_path, + ), + }, + { + "name": "[3]", + "path": os.fspath(param2_path), + "lineno": "6", + "type_": "test", + "id_": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[3]", + param2_path, + ), + "runID": get_absolute_test_id( + "param_same_name/test_param2.py::test_odd_even[3]", + param2_path, + ), + }, + ], + "id_": os.fspath(param2_path) + "::test_odd_even", + } + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +tests_path = TEST_DATA_PATH / "root" / "tests" +tests_a_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" +tests_b_path = TEST_DATA_PATH / "root" / "tests" / "test_b.py" +# This is the expected output for the root folder tests. +# └── tests +# └── test_a.py +# └── test_a_function +# └── test_b.py +# └── test_b_function +root_with_config_expected_output = { + "name": "tests", + "path": os.fspath(tests_path), + "type_": "folder", + "children": [ + { + "name": "test_a.py", + "path": os.fspath(tests_a_path), + "type_": "file", + "id_": os.fspath(tests_a_path), + "children": [ + { + "name": "test_a_function", + "path": os.fspath(os.path.join(tests_path, "test_a.py")), # noqa: PTH118 + "lineno": find_test_line_number( + "test_a_function", + os.path.join(tests_path, "test_a.py"), # noqa: PTH118 + ), + "type_": "test", + "id_": get_absolute_test_id("tests/test_a.py::test_a_function", tests_a_path), + "runID": get_absolute_test_id("tests/test_a.py::test_a_function", tests_a_path), + } + ], + }, + { + "name": "test_b.py", + "path": os.fspath(tests_b_path), + "type_": "file", + "id_": os.fspath(tests_b_path), + "children": [ + { + "name": "test_b_function", + "path": os.fspath(os.path.join(tests_path, "test_b.py")), # noqa: PTH118 + "lineno": find_test_line_number( + "test_b_function", + os.path.join(tests_path, "test_b.py"), # noqa: PTH118 + ), + "type_": "test", + "id_": get_absolute_test_id("tests/test_b.py::test_b_function", tests_b_path), + "runID": get_absolute_test_id("tests/test_b.py::test_b_function", tests_b_path), + } + ], + }, + ], + "id_": os.fspath(tests_path), +} +TEST_MULTI_CLASS_NEST_PATH = TEST_DATA_PATH / "test_multi_class_nest.py" +# This is the expected output for the nested_classes tests. +# └── test_multi_class_nest.py +# └── TestFirstClass +# └── TestSecondClass +# └── test_second +# └── test_first +# └── TestSecondClass2 +# └── test_second2 +# └── test_independent +nested_classes_expected_test_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "test_multi_class_nest.py", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "file", + "id_": str(TEST_MULTI_CLASS_NEST_PATH), + "children": [ + { + "name": "TestFirstClass", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "class", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass", + TEST_MULTI_CLASS_NEST_PATH, + ), + "lineno": find_class_line_number("TestFirstClass", TEST_MULTI_CLASS_NEST_PATH), + "children": [ + { + "name": "TestSecondClass", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "class", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass", + TEST_MULTI_CLASS_NEST_PATH, + ), + "lineno": find_class_line_number( + "TestSecondClass", TEST_MULTI_CLASS_NEST_PATH + ), + "children": [ + { + "name": "test_second", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_second", + str(TEST_MULTI_CLASS_NEST_PATH), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass::test_second", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass::test_second", + TEST_MULTI_CLASS_NEST_PATH, + ), + } + ], + }, + { + "name": "test_first", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_first", str(TEST_MULTI_CLASS_NEST_PATH) + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::test_first", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::test_first", + TEST_MULTI_CLASS_NEST_PATH, + ), + }, + { + "name": "TestSecondClass2", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "type_": "class", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass2", + TEST_MULTI_CLASS_NEST_PATH, + ), + "lineno": find_class_line_number( + "TestSecondClass2", TEST_MULTI_CLASS_NEST_PATH + ), + "children": [ + { + "name": "test_second2", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_second2", + str(TEST_MULTI_CLASS_NEST_PATH), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass2::test_second2", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::TestFirstClass::TestSecondClass2::test_second2", + TEST_MULTI_CLASS_NEST_PATH, + ), + } + ], + }, + ], + }, + { + "name": "test_independent", + "path": str(TEST_MULTI_CLASS_NEST_PATH), + "lineno": find_test_line_number( + "test_independent", str(TEST_MULTI_CLASS_NEST_PATH) + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_multi_class_nest.py::test_independent", + TEST_MULTI_CLASS_NEST_PATH, + ), + "runID": get_absolute_test_id( + "test_multi_class_nest.py::test_independent", + TEST_MULTI_CLASS_NEST_PATH, + ), + }, + ], + } + ], + "id_": str(TEST_DATA_PATH), +} +SYMLINK_FOLDER_PATH = TEST_DATA_PATH / "symlink_folder" +SYMLINK_FOLDER_PATH_TESTS = TEST_DATA_PATH / "symlink_folder" / "tests" +SYMLINK_FOLDER_PATH_TESTS_TEST_A = TEST_DATA_PATH / "symlink_folder" / "tests" / "test_a.py" +SYMLINK_FOLDER_PATH_TESTS_TEST_B = TEST_DATA_PATH / "symlink_folder" / "tests" / "test_b.py" + +# This is the expected output for the symlink_folder tests. +# └── symlink_folder +# └── tests +# └── test_a.py +# └── test_a_function +# └── test_b.py +# └── test_b_function +symlink_expected_discovery_output = { + "name": "symlink_folder", + "path": str(SYMLINK_FOLDER_PATH), + "type_": "folder", + "children": [ + { + "name": "tests", + "path": str(SYMLINK_FOLDER_PATH_TESTS), + "type_": "folder", + "id_": str(SYMLINK_FOLDER_PATH_TESTS), + "children": [ + { + "name": "test_a.py", + "path": str(SYMLINK_FOLDER_PATH_TESTS_TEST_A), + "type_": "file", + "id_": str(SYMLINK_FOLDER_PATH_TESTS_TEST_A), + "children": [ + { + "name": "test_a_function", + "path": str(SYMLINK_FOLDER_PATH_TESTS_TEST_A), + "lineno": find_test_line_number( + "test_a_function", + os.path.join(tests_path, "test_a.py"), # noqa: PTH118 + ), + "type_": "test", + "id_": get_absolute_test_id( + "tests/test_a.py::test_a_function", + SYMLINK_FOLDER_PATH_TESTS_TEST_A, + ), + "runID": get_absolute_test_id( + "tests/test_a.py::test_a_function", + SYMLINK_FOLDER_PATH_TESTS_TEST_A, + ), + } + ], + }, + { + "name": "test_b.py", + "path": str(SYMLINK_FOLDER_PATH_TESTS_TEST_B), + "type_": "file", + "id_": str(SYMLINK_FOLDER_PATH_TESTS_TEST_B), + "children": [ + { + "name": "test_b_function", + "path": str(SYMLINK_FOLDER_PATH_TESTS_TEST_B), + "lineno": find_test_line_number( + "test_b_function", + os.path.join(tests_path, "test_b.py"), # noqa: PTH118 + ), + "type_": "test", + "id_": get_absolute_test_id( + "tests/test_b.py::test_b_function", + SYMLINK_FOLDER_PATH_TESTS_TEST_B, + ), + "runID": get_absolute_test_id( + "tests/test_b.py::test_b_function", + SYMLINK_FOLDER_PATH_TESTS_TEST_B, + ), + } + ], + }, + ], + } + ], + "id_": str(SYMLINK_FOLDER_PATH), +} + +same_function_new_class_param_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "same_function_new_class_param.py", + "path": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "type_": "file", + "id_": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "children": [ + { + "name": "TestNotEmpty", + "path": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "type_": "class", + "children": [ + { + "name": "test_integer", + "path": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "type_": "function", + "children": [ + { + "name": "[1-1]", + "path": os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + "lineno": find_test_line_number( + "TestNotEmpty::test_integer", + os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + ), + "type_": "test", + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_integer[1-1]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "runID": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_integer[1-1]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + }, + { + "name": "[2-2]", + "path": os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + "lineno": find_test_line_number( + "TestNotEmpty::test_integer", + os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + ), + "type_": "test", + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_integer[2-2]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "runID": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_integer[2-2]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + }, + ], + "id_": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py") + + "::TestNotEmpty::test_integer", + }, + { + "name": "test_string", + "path": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "type_": "function", + "children": [ + { + "name": "[a-a]", + "path": os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + "lineno": find_test_line_number( + "TestNotEmpty::test_string", + os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + ), + "type_": "test", + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_string[a-a]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "runID": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_string[a-a]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + }, + { + "name": "[b-b]", + "path": os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + "lineno": find_test_line_number( + "TestNotEmpty::test_string", + os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + ), + "type_": "test", + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_string[b-b]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "runID": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty::test_string[b-b]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + }, + ], + "id_": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py") + + "::TestNotEmpty::test_string", + }, + ], + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestNotEmpty", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "lineno": find_class_line_number( + "TestNotEmpty", TEST_DATA_PATH / "same_function_new_class_param.py" + ), + }, + { + "name": "TestEmpty", + "path": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "type_": "class", + "children": [ + { + "name": "test_integer", + "path": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "type_": "function", + "children": [ + { + "name": "[0-0]", + "path": os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + "lineno": find_test_line_number( + "TestEmpty::test_integer", + os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + ), + "type_": "test", + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestEmpty::test_integer[0-0]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "runID": get_absolute_test_id( + "same_function_new_class_param.py::TestEmpty::test_integer[0-0]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + }, + ], + "id_": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py") + + "::TestEmpty::test_integer", + }, + { + "name": "test_string", + "path": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py"), + "type_": "function", + "children": [ + { + "name": "[-]", + "path": os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + "lineno": find_test_line_number( + "TestEmpty::test_string", + os.fspath( + TEST_DATA_PATH / "same_function_new_class_param.py" + ), + ), + "type_": "test", + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestEmpty::test_string[-]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "runID": get_absolute_test_id( + "same_function_new_class_param.py::TestEmpty::test_string[-]", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + }, + ], + "id_": os.fspath(TEST_DATA_PATH / "same_function_new_class_param.py") + + "::TestEmpty::test_string", + }, + ], + "id_": get_absolute_test_id( + "same_function_new_class_param.py::TestEmpty", + TEST_DATA_PATH / "same_function_new_class_param.py", + ), + "lineno": find_class_line_number( + "TestEmpty", TEST_DATA_PATH / "same_function_new_class_param.py" + ), + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +test_param_span_class_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "test_param_span_class.py", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "type_": "file", + "id_": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "children": [ + { + "name": "TestClass1", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "type_": "class", + "children": [ + { + "name": "test_method1", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "type_": "function", + "children": [ + { + "name": "[1]", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "lineno": find_test_line_number( + "TestClass1::test_method1", + os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_param_span_class.py::TestClass1::test_method1[1]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + "runID": get_absolute_test_id( + "test_param_span_class.py::TestClass1::test_method1[1]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + }, + { + "name": "[2]", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "lineno": find_test_line_number( + "TestClass1::test_method1", + os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_param_span_class.py::TestClass1::test_method1[2]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + "runID": get_absolute_test_id( + "test_param_span_class.py::TestClass1::test_method1[2]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + }, + ], + "id_": os.fspath( + TEST_DATA_PATH + / "test_param_span_class.py::TestClass1::test_method1" + ), + } + ], + "id_": get_absolute_test_id( + "test_param_span_class.py::TestClass1", + TEST_DATA_PATH / "test_param_span_class.py", + ), + "lineno": find_class_line_number( + "TestClass1", TEST_DATA_PATH / "test_param_span_class.py" + ), + }, + { + "name": "TestClass2", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "type_": "class", + "children": [ + { + "name": "test_method1", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "type_": "function", + "children": [ + { + "name": "[1]", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "lineno": find_test_line_number( + "TestClass2::test_method1", + os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_param_span_class.py::TestClass2::test_method1[1]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + "runID": get_absolute_test_id( + "test_param_span_class.py::TestClass2::test_method1[1]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + }, + { + "name": "[2]", + "path": os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + "lineno": find_test_line_number( + "TestClass2::test_method1", + os.fspath(TEST_DATA_PATH / "test_param_span_class.py"), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_param_span_class.py::TestClass2::test_method1[2]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + "runID": get_absolute_test_id( + "test_param_span_class.py::TestClass2::test_method1[2]", + TEST_DATA_PATH / "test_param_span_class.py", + ), + }, + ], + "id_": os.fspath( + TEST_DATA_PATH + / "test_param_span_class.py::TestClass2::test_method1" + ), + } + ], + "id_": get_absolute_test_id( + "test_param_span_class.py::TestClass2", + TEST_DATA_PATH / "test_param_span_class.py", + ), + "lineno": find_class_line_number( + "TestClass2", TEST_DATA_PATH / "test_param_span_class.py" + ), + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} +# This is the expected output for the describe_only.py tests. +# └── describe_only.py +# └── describe_A +# └── test_1 +# └── test_2 + +describe_only_path = TEST_DATA_PATH / "pytest_describe_plugin" / "describe_only.py" +pytest_describe_plugin_path = TEST_DATA_PATH / "pytest_describe_plugin" + +expected_describe_only_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "pytest_describe_plugin", + "path": os.fspath(pytest_describe_plugin_path), + "type_": "folder", + "id_": os.fspath(pytest_describe_plugin_path), + "children": [ + { + "name": "describe_only.py", + "path": os.fspath(describe_only_path), + "type_": "file", + "id_": os.fspath(describe_only_path), + "children": [ + { + "name": "describe_A", + "path": os.fspath(describe_only_path), + "type_": "class", + "children": [ + { + "name": "test_1", + "path": os.fspath(describe_only_path), + "lineno": find_test_line_number( + "test_1", + describe_only_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_1", + describe_only_path, + ), + "runID": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_1", + describe_only_path, + ), + }, + { + "name": "test_2", + "path": os.fspath(describe_only_path), + "lineno": find_test_line_number( + "test_2", + describe_only_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_2", + describe_only_path, + ), + "runID": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_2", + describe_only_path, + ), + }, + ], + "id_": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A", + describe_only_path, + ), + "lineno": find_class_line_number("describe_A", describe_only_path), + } + ], + } + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} +# This is the expected output for the nested_describe.py tests. +# └── nested_describe.py +# └── describe_list +# └── describe_append +# └── add_empty +# └── remove_empty +# └── describe_remove +# └── removes +nested_describe_path = TEST_DATA_PATH / "pytest_describe_plugin" / "nested_describe.py" +expected_nested_describe_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "pytest_describe_plugin", + "path": os.fspath(pytest_describe_plugin_path), + "type_": "folder", + "id_": os.fspath(pytest_describe_plugin_path), + "children": [ + { + "name": "nested_describe.py", + "path": os.fspath(nested_describe_path), + "type_": "file", + "id_": os.fspath(nested_describe_path), + "children": [ + { + "name": "describe_list", + "path": os.fspath(nested_describe_path), + "type_": "class", + "children": [ + { + "name": "describe_append", + "path": os.fspath(nested_describe_path), + "type_": "class", + "children": [ + { + "name": "add_empty", + "path": os.fspath(nested_describe_path), + "lineno": find_test_line_number( + "add_empty", + nested_describe_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::add_empty", + nested_describe_path, + ), + "runID": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::add_empty", + nested_describe_path, + ), + }, + { + "name": "remove_empty", + "path": os.fspath(nested_describe_path), + "lineno": find_test_line_number( + "remove_empty", + nested_describe_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::remove_empty", + nested_describe_path, + ), + "runID": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::remove_empty", + nested_describe_path, + ), + }, + ], + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append", + nested_describe_path, + ), + "lineno": find_class_line_number( + "describe_append", nested_describe_path + ), + }, + { + "name": "describe_remove", + "path": os.fspath(nested_describe_path), + "type_": "class", + "children": [ + { + "name": "removes", + "path": os.fspath(nested_describe_path), + "lineno": find_test_line_number( + "removes", + nested_describe_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove::removes", + nested_describe_path, + ), + "runID": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove::removes", + nested_describe_path, + ), + } + ], + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove", + nested_describe_path, + ), + "lineno": find_class_line_number( + "describe_remove", nested_describe_path + ), + }, + ], + "id_": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list", + nested_describe_path, + ), + "lineno": find_class_line_number("describe_list", nested_describe_path), + } + ], + } + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} +# This is the expected output for the folder_with_script folder when run with ruff +# └── .data +# └── folder_with_script +# └── script_random.py +# └── ruff +# └── test_simple.py +# └── ruff +# └── test_function +ruff_test_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "folder_with_script", + "path": os.fspath(TEST_DATA_PATH / "folder_with_script"), + "type_": "folder", + "id_": os.fspath(TEST_DATA_PATH / "folder_with_script"), + "children": [ + { + "name": "script_random.py", + "path": os.fspath(TEST_DATA_PATH / "folder_with_script" / "script_random.py"), + "type_": "file", + "id_": os.fspath(TEST_DATA_PATH / "folder_with_script" / "script_random.py"), + "children": [ + { + "name": "ruff", + "path": os.fspath( + TEST_DATA_PATH / "folder_with_script" / "script_random.py" + ), + "lineno": "", + "type_": "test", + "id_": get_absolute_test_id( + "folder_with_script/script_random.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "script_random.py", + ), + "runID": get_absolute_test_id( + "folder_with_script/script_random.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "script_random.py", + ), + } + ], + }, + { + "name": "test_simple.py", + "path": os.fspath(TEST_DATA_PATH / "folder_with_script" / "test_simple.py"), + "type_": "file", + "id_": os.fspath(TEST_DATA_PATH / "folder_with_script" / "test_simple.py"), + "children": [ + { + "name": "ruff", + "path": os.fspath( + TEST_DATA_PATH / "folder_with_script" / "test_simple.py" + ), + "lineno": "", + "type_": "test", + "id_": get_absolute_test_id( + "folder_with_script/test_simple.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + "runID": get_absolute_test_id( + "folder_with_script/test_simple.py::ruff", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + }, + { + "name": "test_function", + "path": os.fspath( + TEST_DATA_PATH / "folder_with_script" / "test_simple.py" + ), + "lineno": find_test_line_number( + "test_function", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + "type_": "test", + "id_": get_absolute_test_id( + "folder_with_script/test_simple.py::test_function", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + "runID": get_absolute_test_id( + "folder_with_script/test_simple.py::test_function", + TEST_DATA_PATH / "folder_with_script" / "test_simple.py", + ), + }, + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# This is the expected output for the 2496-black-formatter folder when run with black plugin +# └── .data +# └── 2496-black-formatter +# └── app.py +# └── black +# └── test_app.py +# └── black +# └── test_add +# └── test_subtract +black_formatter_folder_path = TEST_DATA_PATH / "2496-black-formatter" +black_app_path = black_formatter_folder_path / "app.py" +black_test_app_path = black_formatter_folder_path / "test_app.py" +black_formatter_expected_output = { + "name": ".data", + "path": TEST_DATA_PATH_STR, + "type_": "folder", + "children": [ + { + "name": "2496-black-formatter", + "path": os.fspath(black_formatter_folder_path), + "type_": "folder", + "id_": os.fspath(black_formatter_folder_path), + "children": [ + { + "name": "app.py", + "path": os.fspath(black_app_path), + "type_": "file", + "id_": os.fspath(black_app_path), + "children": [ + { + "name": "black", + "path": os.fspath(black_app_path), + "lineno": "0", + "type_": "test", + "id_": get_absolute_test_id( + "2496-black-formatter/app.py::black", + black_app_path, + ), + "runID": get_absolute_test_id( + "2496-black-formatter/app.py::black", + black_app_path, + ), + } + ], + }, + { + "name": "test_app.py", + "path": os.fspath(black_test_app_path), + "type_": "file", + "id_": os.fspath(black_test_app_path), + "children": [ + { + "name": "black", + "path": os.fspath(black_test_app_path), + "lineno": "0", + "type_": "test", + "id_": get_absolute_test_id( + "2496-black-formatter/test_app.py::black", + black_test_app_path, + ), + "runID": get_absolute_test_id( + "2496-black-formatter/test_app.py::black", + black_test_app_path, + ), + }, + { + "name": "test_add", + "path": os.fspath(black_test_app_path), + "lineno": find_test_line_number( + "test_add", + black_test_app_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "2496-black-formatter/test_app.py::test_add", + black_test_app_path, + ), + "runID": get_absolute_test_id( + "2496-black-formatter/test_app.py::test_add", + black_test_app_path, + ), + }, + { + "name": "test_subtract", + "path": os.fspath(black_test_app_path), + "lineno": find_test_line_number( + "test_subtract", + black_test_app_path, + ), + "type_": "test", + "id_": get_absolute_test_id( + "2496-black-formatter/test_app.py::test_subtract", + black_test_app_path, + ), + "runID": get_absolute_test_id( + "2496-black-formatter/test_app.py::test_subtract", + black_test_app_path, + ), + }, + ], + }, + ], + } + ], + "id_": TEST_DATA_PATH_STR, +} + +# ===================================================================================== +# PROJECT_ROOT_PATH environment variable tests +# These test the project-based testing feature where PROJECT_ROOT_PATH changes +# the test tree root from cwd to the specified project path. +# ===================================================================================== + +# This is the expected output for unittest_folder when PROJECT_ROOT_PATH is set to unittest_folder. +# The root of the tree is unittest_folder (not .data), simulating project-based testing. +# +# **Project Configuration:** +# In the VS Code Python extension, projects are defined by the Python Environments extension. +# Each project has a root directory (identified by pyproject.toml, setup.py, etc.). +# When PROJECT_ROOT_PATH is set, pytest uses that path as the test tree root instead of cwd. +# +# **Test Tree Structure:** +# Without PROJECT_ROOT_PATH (legacy mode): +# └── .data (cwd = workspace root) +# └── unittest_folder +# └── test_add.py, test_subtract.py... +# +# With PROJECT_ROOT_PATH set to unittest_folder (project-based mode): +# └── unittest_folder (ROOT - set via PROJECT_ROOT_PATH env var) +# ├── test_add.py +# │ └── TestAddFunction +# │ ├── test_add_negative_numbers +# │ └── test_add_positive_numbers +# │ └── TestDuplicateFunction +# │ └── test_dup_a +# └── test_subtract.py +# └── TestSubtractFunction +# ├── test_subtract_negative_numbers +# └── test_subtract_positive_numbers +# └── TestDuplicateFunction +# └── test_dup_s +# +# Note: This reuses the unittest_folder paths defined earlier in this file. +project_root_unittest_folder_expected_output = { + "name": "unittest_folder", + "path": os.fspath(unittest_folder_path), + "type_": "folder", + "children": [ + { + "name": "test_add.py", + "path": os.fspath(test_add_path), + "type_": "file", + "id_": os.fspath(test_add_path), + "children": [ + { + "name": "TestAddFunction", + "path": os.fspath(test_add_path), + "type_": "class", + "children": [ + { + "name": "test_add_negative_numbers", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_add_negative_numbers", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "test_add.py::TestAddFunction::test_add_negative_numbers", + test_add_path, + ), + }, + { + "name": "test_add_positive_numbers", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_add_positive_numbers", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), + "runID": get_absolute_test_id( + "test_add.py::TestAddFunction::test_add_positive_numbers", + test_add_path, + ), + }, + ], + "id_": get_absolute_test_id( + "test_add.py::TestAddFunction", + test_add_path, + ), + "lineno": find_class_line_number("TestAddFunction", test_add_path), + }, + { + "name": "TestDuplicateFunction", + "path": os.fspath(test_add_path), + "type_": "class", + "children": [ + { + "name": "test_dup_a", + "path": os.fspath(test_add_path), + "lineno": find_test_line_number( + "test_dup_a", + os.fspath(test_add_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), + "runID": get_absolute_test_id( + "test_add.py::TestDuplicateFunction::test_dup_a", + test_add_path, + ), + }, + ], + "id_": get_absolute_test_id( + "test_add.py::TestDuplicateFunction", + test_add_path, + ), + "lineno": find_class_line_number("TestDuplicateFunction", test_add_path), + }, + ], + }, + { + "name": "test_subtract.py", + "path": os.fspath(test_subtract_path), + "type_": "file", + "id_": os.fspath(test_subtract_path), + "children": [ + { + "name": "TestSubtractFunction", + "path": os.fspath(test_subtract_path), + "type_": "class", + "children": [ + { + "name": "test_subtract_negative_numbers", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_subtract_negative_numbers", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + test_subtract_path, + ), + }, + { + "name": "test_subtract_positive_numbers", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_subtract_positive_numbers", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + test_subtract_path, + ), + }, + ], + "id_": get_absolute_test_id( + "test_subtract.py::TestSubtractFunction", + test_subtract_path, + ), + "lineno": find_class_line_number("TestSubtractFunction", test_subtract_path), + }, + { + "name": "TestDuplicateFunction", + "path": os.fspath(test_subtract_path), + "type_": "class", + "children": [ + { + "name": "test_dup_s", + "path": os.fspath(test_subtract_path), + "lineno": find_test_line_number( + "test_dup_s", + os.fspath(test_subtract_path), + ), + "type_": "test", + "id_": get_absolute_test_id( + "test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), + "runID": get_absolute_test_id( + "test_subtract.py::TestDuplicateFunction::test_dup_s", + test_subtract_path, + ), + }, + ], + "id_": get_absolute_test_id( + "test_subtract.py::TestDuplicateFunction", + test_subtract_path, + ), + "lineno": find_class_line_number("TestDuplicateFunction", test_subtract_path), + }, + ], + }, + ], + "id_": os.fspath(unittest_folder_path), +} diff --git a/python_files/tests/pytestadapter/expected_execution_test_output.py b/python_files/tests/pytestadapter/expected_execution_test_output.py new file mode 100644 index 000000000000..fa6743d0e112 --- /dev/null +++ b/python_files/tests/pytestadapter/expected_execution_test_output.py @@ -0,0 +1,749 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from .helpers import TEST_DATA_PATH, get_absolute_test_id + +TEST_SUBTRACT_FUNCTION = "unittest_folder/test_subtract.py::TestSubtractFunction::" +TEST_ADD_FUNCTION = "unittest_folder/test_add.py::TestAddFunction::" +SUCCESS = "success" +FAILURE = "failure" + +# This is the expected output for the unittest_folder execute tests +# └── unittest_folder +# ├── test_add.py +# │ └── TestAddFunction +# │ ├── test_add_negative_numbers: success +# │ └── test_add_positive_numbers: success +# └── test_subtract.py +# └── TestSubtractFunction +# ├── test_subtract_negative_numbers: failure +# └── test_subtract_positive_numbers: success +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" +uf_execution_expected_output = { + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + test_subtract_path, + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_negative_numbers", + test_subtract_path, + ), + "outcome": FAILURE, + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, +} + + +# This is the expected output for the unittest_folder only execute add.py tests +# └── unittest_folder +# ├── test_add.py +# │ └── TestAddFunction +# │ ├── test_add_negative_numbers: success +# │ └── test_add_positive_numbers: success +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" + +uf_single_file_expected_output = { + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_negative_numbers", test_add_path + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, +} + + +# This is the expected output for the unittest_folder execute only signle method +# └── unittest_folder +# ├── test_add.py +# │ └── TestAddFunction +# │ └── test_add_positive_numbers: success +uf_single_method_execution_expected_output = { + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the unittest_folder tests run where two tests +# run are in different files. +# └── unittest_folder +# ├── test_add.py +# │ └── TestAddFunction +# │ └── test_add_positive_numbers: success +# └── test_subtract.py +# └── TestSubtractFunction +# └── test_subtract_positive_numbers: success +test_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" +test_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" + +uf_non_adjacent_tests_execution_expected_output = { + get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", test_subtract_path + ): { + "test": get_absolute_test_id( + f"{TEST_SUBTRACT_FUNCTION}test_subtract_positive_numbers", + test_subtract_path, + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id(f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path): { + "test": get_absolute_test_id( + f"{TEST_ADD_FUNCTION}test_add_positive_numbers", test_add_path + ), + "outcome": SUCCESS, + "message": None, + "traceback": None, + "subtest": None, + }, +} + + +# This is the expected output for the simple_pytest.py file. +# └── simple_pytest.py +# └── test_function: success +simple_pytest_path = TEST_DATA_PATH / "unittest_folder" / "simple_pytest.py" + +simple_execution_pytest_expected_output = { + get_absolute_test_id("test_function", simple_pytest_path): { + "test": get_absolute_test_id("test_function", simple_pytest_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} + + +# This is the expected output for the unittest_pytest_same_file.py file. +# ├── unittest_pytest_same_file.py +# ├── TestExample +# │ └── test_true_unittest: success +# └── test_true_pytest: success +unit_pytest_same_file_path = TEST_DATA_PATH / "unittest_pytest_same_file.py" +unit_pytest_same_file_execution_expected_output = { + get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ): { + "test": get_absolute_test_id( + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + unit_pytest_same_file_path, + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", unit_pytest_same_file_path + ): { + "test": get_absolute_test_id( + "unittest_pytest_same_file.py::test_true_pytest", + unit_pytest_same_file_path, + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the error_raised_exception.py file. +# └── error_raise_exception.py +# ├── TestSomething +# │ └── test_a: failure +error_raised_exception_path = TEST_DATA_PATH / "error_raise_exception.py" +error_raised_exception_execution_expected_output = { + get_absolute_test_id( + "error_raise_exception.py::TestSomething::test_a", error_raised_exception_path + ): { + "test": get_absolute_test_id( + "error_raise_exception.py::TestSomething::test_a", + error_raised_exception_path, + ), + "outcome": "error", + "message": "ERROR MESSAGE", + "traceback": "TRACEBACK", + "subtest": None, + } +} + +# This is the expected output for the skip_tests.py file. +# └── test_something: success +# └── test_another_thing: skipped +# └── test_decorator_thing: skipped +# └── test_decorator_thing_2: skipped +# ├── TestClass +# │ └── test_class_function_a: skipped +# │ └── test_class_function_b: skipped + +skip_tests_path = TEST_DATA_PATH / "skip_tests.py" +skip_tests_execution_expected_output = { + get_absolute_test_id("skip_tests.py::test_something", skip_tests_path): { + "test": get_absolute_test_id("skip_tests.py::test_something", skip_tests_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("skip_tests.py::test_another_thing", skip_tests_path): { + "test": get_absolute_test_id("skip_tests.py::test_another_thing", skip_tests_path), + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("skip_tests.py::test_decorator_thing", skip_tests_path): { + "test": get_absolute_test_id("skip_tests.py::test_decorator_thing", skip_tests_path), + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("skip_tests.py::test_decorator_thing_2", skip_tests_path): { + "test": get_absolute_test_id("skip_tests.py::test_decorator_thing_2", skip_tests_path), + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("skip_tests.py::TestClass::test_class_function_a", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_a", skip_tests_path + ), + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("skip_tests.py::TestClass::test_class_function_b", skip_tests_path): { + "test": get_absolute_test_id( + "skip_tests.py::TestClass::test_class_function_b", skip_tests_path + ), + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + }, +} + + +# This is the expected output for the dual_level_nested_folder.py tests +# └── dual_level_nested_folder +# └── test_top_folder.py +# └── test_top_function_t: success +# └── test_top_function_f: failure +# └── nested_folder_one +# └── test_bottom_folder.py +# └── test_bottom_function_t: success +# └── test_bottom_function_f: failure +dual_level_nested_folder_top_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" +) +dual_level_nested_folder_bottom_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" +) +dual_level_nested_folder_execution_expected_output = { + get_absolute_test_id( + "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path + ): { + "test": get_absolute_test_id( + "test_top_folder.py::test_top_function_t", dual_level_nested_folder_top_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path + ): { + "test": get_absolute_test_id( + "test_top_folder.py::test_top_function_f", dual_level_nested_folder_top_path + ), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + dual_level_nested_folder_bottom_path, + ): { + "test": get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + dual_level_nested_folder_bottom_path, + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + dual_level_nested_folder_bottom_path, + ): { + "test": get_absolute_test_id( + "nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + dual_level_nested_folder_bottom_path, + ), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the nested_folder tests. +# └── folder_a +# └── folder_b +# └── folder_a +# └── test_nest.py +# └── test_function: success + +nested_folder_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +double_nested_folder_expected_execution_output = { + get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path + ): { + "test": get_absolute_test_id( + "folder_a/folder_b/folder_a/test_nest.py::test_function", nested_folder_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} +# This is the expected output for the nested_folder tests. +# └── parametrize_tests.py +# └── TestClass +# └── test_adding[3+5-8]: success +# └── test_adding[2+4-6]: success +# └── test_adding[6+9-16]: failure +parametrize_tests_path = TEST_DATA_PATH / "parametrize_tests.py" + +parametrize_tests_expected_execution_output = { + get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[3+5-8]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[3+5-8]", parametrize_tests_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[2+4-6]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[2+4-6]", parametrize_tests_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[6+9-16]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[6+9-16]", parametrize_tests_path + ), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the single parameterized tests. +# └── parametrize_tests.py +# └── TestClass +# └── test_adding[3+5-8]: success +single_parametrize_tests_expected_execution_output = { + get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[3+5-8]", parametrize_tests_path + ): { + "test": get_absolute_test_id( + "parametrize_tests.py::TestClass::test_adding[3+5-8]", parametrize_tests_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the single parameterized tests. +# └── text_docstring.txt +# └── text_docstring: success +doc_test_path = TEST_DATA_PATH / "text_docstring.txt" +doctest_pytest_expected_execution_output = { + get_absolute_test_id("text_docstring.txt::text_docstring.txt", doc_test_path): { + "test": get_absolute_test_id("text_docstring.txt::text_docstring.txt", doc_test_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} + +# Will run all tests in the cwd that fit the test file naming pattern. +folder_a_path = TEST_DATA_PATH / "folder_a" / "folder_b" / "folder_a" / "test_nest.py" +dual_level_nested_folder_top_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "test_top_folder.py" +) +dual_level_nested_folder_bottom_path = ( + TEST_DATA_PATH / "dual_level_nested_folder" / "nested_folder_one" / "test_bottom_folder.py" +) +unittest_folder_add_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" +unittest_folder_subtract_path = TEST_DATA_PATH / "unittest_folder" / "test_subtract.py" + +no_test_ids_pytest_execution_expected_output = { + get_absolute_test_id("test_function", folder_a_path): { + "test": get_absolute_test_id("test_function", folder_a_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_top_function_t", dual_level_nested_folder_top_path): { + "test": get_absolute_test_id("test_top_function_t", dual_level_nested_folder_top_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_top_function_f", dual_level_nested_folder_top_path): { + "test": get_absolute_test_id("test_top_function_f", dual_level_nested_folder_top_path), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_bottom_function_t", dual_level_nested_folder_bottom_path): { + "test": get_absolute_test_id( + "test_bottom_function_t", dual_level_nested_folder_bottom_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_bottom_function_f", dual_level_nested_folder_bottom_path): { + "test": get_absolute_test_id( + "test_bottom_function_f", dual_level_nested_folder_bottom_path + ), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("TestAddFunction::test_add_negative_numbers", unittest_folder_add_path): { + "test": get_absolute_test_id( + "TestAddFunction::test_add_negative_numbers", unittest_folder_add_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("TestAddFunction::test_add_positive_numbers", unittest_folder_add_path): { + "test": get_absolute_test_id( + "TestAddFunction::test_add_positive_numbers", unittest_folder_add_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "TestSubtractFunction::test_subtract_negative_numbers", + unittest_folder_subtract_path, + ): { + "test": get_absolute_test_id( + "TestSubtractFunction::test_subtract_negative_numbers", + unittest_folder_subtract_path, + ), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "TestSubtractFunction::test_subtract_positive_numbers", + unittest_folder_subtract_path, + ): { + "test": get_absolute_test_id( + "TestSubtractFunction::test_subtract_positive_numbers", + unittest_folder_subtract_path, + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the root folder with the config file referenced. +# └── test_a.py +# └── test_a_function: success +test_add_path = TEST_DATA_PATH / "root" / "tests" / "test_a.py" +config_file_pytest_expected_execution_output = { + get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path): { + "test": get_absolute_test_id("tests/test_a.py::test_a_function", test_add_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} + + +# This is the expected output for the test logging file. +# └── test_logging.py +# └── test_logging2: failure +# └── test_logging: success +test_logging_path = TEST_DATA_PATH / "test_logging.py" + +logging_test_expected_execution_output = { + get_absolute_test_id("test_logging.py::test_logging2", test_logging_path): { + "test": get_absolute_test_id("test_logging.py::test_logging2", test_logging_path), + "outcome": "failure", + "message": "ERROR MESSAGE", + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_logging.py::test_logging", test_logging_path): { + "test": get_absolute_test_id("test_logging.py::test_logging", test_logging_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the test safe clear env vars file. +# └── test_env_vars.py +# └── test_clear_env: success +# └── test_check_env: success + +test_safe_clear_env_vars_path = TEST_DATA_PATH / "test_env_vars.py" +safe_clear_env_vars_expected_execution_output = { + get_absolute_test_id("test_env_vars.py::test_clear_env", test_safe_clear_env_vars_path): { + "test": get_absolute_test_id( + "test_env_vars.py::test_clear_env", test_safe_clear_env_vars_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id("test_env_vars.py::test_check_env", test_safe_clear_env_vars_path): { + "test": get_absolute_test_id( + "test_env_vars.py::test_check_env", test_safe_clear_env_vars_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the test unsafe clear env vars file. +# └── test_env_vars.py +# └── test_clear_env_unsafe: success +# └── test_check_env_unsafe: success +unsafe_clear_env_vars_expected_execution_output = { + get_absolute_test_id( + "test_env_vars.py::test_clear_env_unsafe", test_safe_clear_env_vars_path + ): { + "test": get_absolute_test_id( + "test_env_vars.py::test_clear_env_unsafe", test_safe_clear_env_vars_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "test_env_vars.py::test_check_env_unsafe", test_safe_clear_env_vars_path + ): { + "test": get_absolute_test_id( + "test_env_vars.py::test_check_env_unsafe", test_safe_clear_env_vars_path + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# Constant for the symlink execution test where TEST_DATA_PATH / "root" the target and TEST_DATA_PATH / "symlink_folder" the symlink +test_a_symlink_path = TEST_DATA_PATH / "symlink_folder" / "tests" / "test_a.py" +symlink_run_expected_execution_output = { + get_absolute_test_id("test_a.py::test_a_function", test_a_symlink_path): { + "test": get_absolute_test_id("test_a.py::test_a_function", test_a_symlink_path), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + } +} + + +# This is the expected output for the pytest_describe_plugin/describe_only.py file. +# └── pytest_describe_plugin +# └── describe_only.py +# └── describe_A +# └── test_1: success +# └── test_2: success + +describe_only_expected_execution_output = { + get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_1", + TEST_DATA_PATH / "pytest_describe_plugin" / "describe_only.py", + ): { + "test": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_1", + TEST_DATA_PATH / "pytest_describe_plugin" / "describe_only.py", + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_2", + TEST_DATA_PATH / "pytest_describe_plugin" / "describe_only.py", + ): { + "test": get_absolute_test_id( + "pytest_describe_plugin/describe_only.py::describe_A::test_2", + TEST_DATA_PATH / "pytest_describe_plugin" / "describe_only.py", + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +# This is the expected output for the pytest_describe_plugin/nested_describe.py file. +# └── pytest_describe_plugin +# └── nested_describe.py +# └── describe_list +# └── describe_append +# └── add_empty: success +# └── remove_empty: success +# └── describe_remove +# └── removes: success +nested_describe_expected_execution_output = { + get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::add_empty", + TEST_DATA_PATH / "pytest_describe_plugin" / "nested_describe.py", + ): { + "test": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::add_empty", + TEST_DATA_PATH / "pytest_describe_plugin" / "nested_describe.py", + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::remove_empty", + TEST_DATA_PATH / "pytest_describe_plugin" / "nested_describe.py", + ): { + "test": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::remove_empty", + TEST_DATA_PATH / "pytest_describe_plugin" / "nested_describe.py", + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, + get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove::removes", + TEST_DATA_PATH / "pytest_describe_plugin" / "nested_describe.py", + ): { + "test": get_absolute_test_id( + "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove::removes", + TEST_DATA_PATH / "pytest_describe_plugin" / "nested_describe.py", + ), + "outcome": "success", + "message": None, + "traceback": None, + "subtest": None, + }, +} + +skip_test_fixture_path = TEST_DATA_PATH / "skip_test_fixture.py" +skip_test_fixture_execution_expected_output = { + get_absolute_test_id("skip_test_fixture.py::test_docker_client", skip_test_fixture_path): { + "test": get_absolute_test_id( + "skip_test_fixture.py::test_docker_client", skip_test_fixture_path + ), + "outcome": "skipped", + "message": None, + "traceback": None, + "subtest": None, + } +} diff --git a/python_files/tests/pytestadapter/helpers.py b/python_files/tests/pytestadapter/helpers.py new file mode 100644 index 000000000000..03f1187149df --- /dev/null +++ b/python_files/tests/pytestadapter/helpers.py @@ -0,0 +1,469 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import contextlib +import io +import json +import os +import pathlib +import socket +import subprocess +import sys +import tempfile +import threading +import uuid +from typing import Any, Dict, List, Optional, Tuple + +if sys.platform == "win32": + from namedpipe import NPopen + + +script_dir = pathlib.Path(__file__).parent.parent.parent +script_dir_child = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir_child)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) +print("sys add path", script_dir) + +TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" +CONTENT_LENGTH: str = "Content-Length:" +CONTENT_TYPE: str = "Content-Type:" + + +@contextlib.contextmanager +def text_to_python_file(text_file_path: pathlib.Path): + """Convert a text file to a python file and yield the python file path.""" + python_file = None + try: + contents = text_file_path.read_text(encoding="utf-8") + python_file = text_file_path.with_suffix(".py") + python_file.write_text(contents, encoding="utf-8") + yield python_file + finally: + if python_file: + python_file.unlink() + + +@contextlib.contextmanager +def create_symlink(root: pathlib.Path, target_ext: str, destination_ext: str): + destination = None + try: + destination = root / destination_ext + target = root / target_ext + if destination and destination.exists(): + print("destination already exists", destination) + try: + destination.symlink_to(target) + except Exception as e: + print("error occurred when attempting to create a symlink", e) + yield target, destination + finally: + if destination and destination.exists(): + destination.unlink() + print("destination unlinked", destination) + + +def process_data_received(data: str) -> List[Dict[str, Any]]: + """Process the all JSON data which comes from the server. + + After listen is finished, this function will be called. + Here the data must be split into individual JSON messages and then parsed. + + This function also: + - Checks that the jsonrpc value is 2.0 + """ + json_messages = [] + remaining = data + while remaining: + json_data, remaining = parse_rpc_message(remaining) + # here json_data is a single rpc payload, now check its jsonrpc 2 and save the param data + if "params" not in json_data or "jsonrpc" not in json_data: + raise ValueError("Invalid JSON-RPC message received, missing params or jsonrpc key") + elif json_data["jsonrpc"] != "2.0": + raise ValueError("Invalid JSON-RPC version received, not version 2.0") + else: + json_messages.append(json_data["params"]) + + return json_messages # return the list of json messages + + +def parse_rpc_message(data: str) -> Tuple[Dict[str, str], str]: + """Process the JSON data which comes from the server. + + A single rpc payload is in the format: + content-length: #LEN# \r\ncontent-type: application/json\r\n\r\n{"jsonrpc": "2.0", "params": ENTIRE_DATA} + + returns: + json_data: A single rpc payload of JSON data from the server. + remaining: The remaining data after the JSON data. + """ + str_stream: io.StringIO = io.StringIO(data) + + length: int = 0 + while True: + line: str = str_stream.readline() + if CONTENT_LENGTH.lower() in line.lower(): + length = int(line[len(CONTENT_LENGTH) :]) + + line: str = str_stream.readline() + if CONTENT_TYPE.lower() not in line.lower(): + raise ValueError("Header does not contain Content-Type") + + line = str_stream.readline() + if line not in ["\r\n", "\n"]: + raise ValueError("Header does not contain space to separate header and body") + # if it passes all these checks then it has the right headers + break + + if not line or line.isspace(): + raise ValueError("Header does not contain Content-Length") + + while True: # keep reading until the number of bytes is the CONTENT_LENGTH + line: str = str_stream.readline(length) + try: + # try to parse the json, if successful it is single payload so return with remaining data + json_data: dict[str, str] = json.loads(line) + return json_data, str_stream.read() + except json.JSONDecodeError: + print("json decode error") + + +def _listen_on_fifo(pipe_name: str, result: List[str], completed: threading.Event): + # Open the FIFO for reading + fifo_path = pathlib.Path(pipe_name) + with fifo_path.open() as fifo: + print("Waiting for data...") + while True: + if completed.is_set(): + break # Exit loop if completed event is set + data = fifo.read() # This will block until data is available + if len(data) == 0: + # If data is empty, assume EOF + break + print(f"Received: {data}") + result.append(data) + + +def _listen_on_pipe_new(listener, result: List[str], completed: threading.Event): + """Listen on the named pipe or Unix domain socket for JSON data from the server. + + Created as a separate function for clarity in threading context. + """ + # Windows design + if sys.platform == "win32": + all_data: list = [] + stream = listener.wait() + while True: + # Read data from collection + close = stream.closed + if close: + break + data = stream.readlines() + if not data: + if completed.is_set(): + break # Exit loop if completed event is set + else: + try: + # Attempt to accept another connection if the current one closes unexpectedly + print("attempt another connection") + except socket.timeout: + # On timeout, append all collected data to result and return + # result.append("".join(all_data)) + return + data_decoded = "".join(data) + all_data.append(data_decoded) + # Append all collected data to result array + result.append("".join(all_data)) + else: # Unix design + connection, _ = listener.socket.accept() + listener.socket.settimeout(1) + all_data: list = [] + while True: + # Reading from connection + data: bytes = connection.recv(1024 * 1024) + if not data: + if completed.is_set(): + break # Exit loop if completed event is set + else: + try: + # Attempt to accept another connection if the current one closes unexpectedly + connection, _ = listener.socket.accept() + except socket.timeout: + # On timeout, append all collected data to result and return + result.append("".join(all_data)) + return + all_data.append(data.decode("utf-8")) + # Append all collected data to result array + result.append("".join(all_data)) + + +def _run_test_code(proc_args: List[str], proc_env, proc_cwd: str, completed: threading.Event): + result = subprocess.run(proc_args, env=proc_env, cwd=proc_cwd) + completed.set() + return result + + +def runner(args: List[str]) -> Optional[List[Dict[str, Any]]]: + """Run a subprocess and a named-pipe to listen for messages at the same time with threading.""" + print("\n Running python test subprocess with cwd set to: ", TEST_DATA_PATH) + return runner_with_cwd(args, TEST_DATA_PATH) + + +def runner_with_cwd(args: List[str], path: pathlib.Path) -> Optional[List[Dict[str, Any]]]: + """Run a subprocess and a named-pipe to listen for messages at the same time with threading.""" + return runner_with_cwd_env(args, path, {}) + + +def split_array_at_item(arr: List[str], item: str) -> Tuple[List[str], List[str]]: + """ + Splits an array into two subarrays at the specified item. + + Args: + arr (List[str]): The array to be split. + item (str): The item at which to split the array. + + Returns: + Tuple[List[str], List[str]]: A tuple containing two subarrays. The first subarray includes the item and all elements before it. The second subarray includes all elements after the item. If the item is not found, the first subarray is the original array and the second subarray is empty. + """ + if item in arr: + index = arr.index(item) + before = arr[: index + 1] + after = arr[index + 1 :] + return before, after + else: + return arr, [] + + +def runner_with_cwd_env( + args: List[str], path: pathlib.Path, env_add: Dict[str, str] +) -> Optional[List[Dict[str, Any]]]: + """ + Run a subprocess and a named-pipe to listen for messages at the same time with threading. + + Includes environment variables to add to the test environment. + """ + process_args: List[str] + pipe_name: str + if "MANAGE_PY_PATH" in env_add and "COVERAGE_ENABLED" not in env_add: + # If we are running Django, generate a unittest-specific pipe name. + process_args = [sys.executable, *args] + pipe_name = generate_random_pipe_name("unittest-discovery-test") + elif "_TEST_VAR_UNITTEST" in env_add: + before_args, after_ids = split_array_at_item(args, "*test*.py") + process_args = [sys.executable, *before_args] + pipe_name = generate_random_pipe_name("unittest-execution-test") + test_ids_pipe = os.fspath( + script_dir / "tests" / "unittestadapter" / ".data" / "coverage_ex" / "10943021.txt" + ) + env_add.update({"RUN_TEST_IDS_PIPE": test_ids_pipe}) + test_ids_arr = after_ids + with open(test_ids_pipe, "w") as f: # noqa: PTH123 + f.write("\n".join(test_ids_arr)) + else: + process_args = [sys.executable, "-m", "pytest", "-p", "vscode_pytest", "-s", *args] + pipe_name = generate_random_pipe_name("pytest-discovery-test") + + if "COVERAGE_ENABLED" in env_add and "_TEST_VAR_UNITTEST" not in env_add: + if "_PYTEST_MANUAL_PLUGIN_LOAD" in env_add: + # Test manual plugin loading scenario for issue #25590 + process_args = [ + sys.executable, + "-m", + "pytest", + "--disable-plugin-autoload", + "-p", + "pytest_cov.plugin", + "-p", + "vscode_pytest", + "--cov=.", + "--cov-branch", + "-s", + *args, + ] + else: + process_args = [ + sys.executable, + "-m", + "pytest", + "-p", + "vscode_pytest", + "--cov=.", + "--cov-branch", + "-s", + *args, + ] + + # Generate pipe name, pipe name specific per OS type. + + # Windows design + if sys.platform == "win32": + with NPopen("r+t", name=pipe_name, bufsize=0) as pipe: + # Update the environment with the pipe name and PYTHONPATH. + env = os.environ.copy() + env.update( + { + "TEST_RUN_PIPE": pipe.path, + "PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent), + } + ) + # if additional environment variables are passed, add them to the environment + if env_add: + env.update(env_add) + + completed = threading.Event() + + result = [] # result is a string array to store the data during threading + t1: threading.Thread = threading.Thread( + target=_listen_on_pipe_new, args=(pipe, result, completed) + ) + t1.start() + + t2 = threading.Thread( + target=_run_test_code, + args=(process_args, env, path, completed), + ) + t2.start() + + t1.join() + t2.join() + + return process_data_received(result[0]) if result else None + else: # Unix design + # Update the environment with the pipe name and PYTHONPATH. + env = os.environ.copy() + env.update( + { + "TEST_RUN_PIPE": pipe_name, + "PYTHONPATH": os.fspath(pathlib.Path(__file__).parent.parent.parent), + } + ) + # if additional environment variables are passed, add them to the environment + if env_add: + env.update(env_add) + # server = UnixPipeServer(pipe_name) + # server.start() + ################# + # Create the FIFO (named pipe) if it doesn't exist + # if not pathlib.Path.exists(pipe_name): + os.mkfifo(pipe_name) + ################# + + completed = threading.Event() + + result = [] # result is a string array to store the data during threading + t1: threading.Thread = threading.Thread( + target=_listen_on_fifo, args=(pipe_name, result, completed) + ) + t1.start() + + t2: threading.Thread = threading.Thread( + target=_run_test_code, + args=(process_args, env, path, completed), + ) + + t2.start() + + t1.join() + t2.join() + + return process_data_received(result[0]) if result else None + + +def find_test_line_number(test_name: str, test_file_path) -> str: + """Function which finds the correct line number for a test by looking for the "test_marker--[test_name]" string. + + The test_name is split on the "[" character to remove the parameterization information. + + Args: + test_name: The name of the test to find the line number for, will be unique per file. + test_file_path: The path to the test file where the test is located. + """ + test_file_unique_id: str = "test_marker--" + test_name.split("[")[0] + with open(test_file_path) as f: # noqa: PTH123 + for i, line in enumerate(f): + if test_file_unique_id in line: + return str(i + 1) + error_str: str = f"Test {test_name!r} not found on any line in {test_file_path}" + raise ValueError(error_str) + + +def find_class_line_number(class_name: str, test_file_path) -> str: + """Function which finds the correct line number for a class definition. + + Args: + class_name: The name of the class to find the line number for. + test_file_path: The path to the test file where the class is located. + """ + # Look for the class definition line (or function for pytest-describe) + with open(test_file_path) as f: # noqa: PTH123 + for i, line in enumerate(f): + # Match "class ClassName" or "class ClassName(" or "class ClassName:" + # Also match "def ClassName(" for pytest-describe blocks + if ( + line.strip().startswith(f"class {class_name}") + or line.strip().startswith(f"class {class_name}(") + or line.strip().startswith(f"def {class_name}(") + ): + return str(i + 1) + error_str: str = f"Class {class_name!r} not found on any line in {test_file_path}" + raise ValueError(error_str) + + +def get_absolute_test_id(test_id: str, test_path: pathlib.Path) -> str: + """Get the absolute test id by joining the testPath with the test_id.""" + split_id = test_id.split("::")[1:] + return "::".join([str(test_path), *split_id]) + + +def generate_random_pipe_name(prefix=""): + # Generate a random suffix using UUID4, ensuring uniqueness. + random_suffix = uuid.uuid4().hex[:10] + # Default prefix if not provided. + if not prefix: + prefix = "python-ext-rpc" + + # For Windows, named pipes have a specific naming convention. + if sys.platform == "win32": + return f"\\\\.\\pipe\\{prefix}-{random_suffix}" + + # For Unix-like systems, use either the XDG_RUNTIME_DIR or a temporary directory. + xdg_runtime_dir = os.getenv("XDG_RUNTIME_DIR") + if xdg_runtime_dir: + return os.path.join(xdg_runtime_dir, f"{prefix}-{random_suffix}") # noqa: PTH118 + else: + return os.path.join(tempfile.gettempdir(), f"{prefix}-{random_suffix}") # noqa: PTH118 + + +class UnixPipeServer: + def __init__(self, name): + self.name = name + self.is_windows = sys.platform == "win32" + if self.is_windows: + raise NotImplementedError( + "This class is only intended for Unix-like systems, not Windows." + ) + else: + # For Unix-like systems, use a Unix domain socket. + self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + # Ensure the socket does not already exist + try: + os.unlink(self.name) # noqa: PTH108 + except OSError: + if os.path.exists(self.name): # noqa: PTH110 + raise + + def start(self): + if self.is_windows: + raise NotImplementedError( + "This class is only intended for Unix-like systems, not Windows." + ) + else: + # Bind the socket to the address and listen for incoming connections. + self.socket.bind(self.name) + self.socket.listen(1) + print(f"Server listening on {self.name}") + + def stop(self): + # Clean up the server socket. + self.socket.close() + print("Server stopped.") diff --git a/python_files/tests/pytestadapter/test_coverage.py b/python_files/tests/pytestadapter/test_coverage.py new file mode 100644 index 000000000000..f2387527698f --- /dev/null +++ b/python_files/tests/pytestadapter/test_coverage.py @@ -0,0 +1,164 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import json +import os +import pathlib +import sys + +import coverage +import pytest +from packaging.version import Version + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) + +from .helpers import ( # noqa: E402 + TEST_DATA_PATH, + runner_with_cwd_env, +) + + +def test_simple_pytest_coverage(): + """ + Test coverage payload is correct for simple pytest example. Output of coverage run is below. + + Name Stmts Miss Branch BrPart Cover + --------------------------------------------------- + __init__.py 0 0 0 0 100% + reverse.py 13 3 8 2 76% + test_reverse.py 11 0 0 0 100% + --------------------------------------------------- + TOTAL 24 3 8 2 84% + + """ + args = [] + env_add = {"COVERAGE_ENABLED": "True"} + cov_folder_path = TEST_DATA_PATH / "coverage_gen" + actual = runner_with_cwd_env(args, cov_folder_path, env_add) + assert actual + cov = actual[-1] + assert cov + results = cov["result"] + assert results + assert len(results) == 3 + focal_function_coverage = results.get(os.fspath(TEST_DATA_PATH / "coverage_gen" / "reverse.py")) + assert focal_function_coverage + assert focal_function_coverage.get("lines_covered") is not None + assert focal_function_coverage.get("lines_missed") is not None + assert set(focal_function_coverage.get("lines_covered")) == {4, 5, 7, 9, 10, 11, 12, 13, 14, 17} + assert len(set(focal_function_coverage.get("lines_missed"))) >= 3 + + coverage_version = Version(coverage.__version__) + # only include check for branches if the version is >= 7.7.0 + if coverage_version >= Version("7.7.0"): + assert focal_function_coverage.get("executed_branches") == 4 + assert focal_function_coverage.get("total_branches") == 6 + + +coverage_gen_file_path = TEST_DATA_PATH / "coverage_gen" / "coverage.json" + + +@pytest.fixture +def cleanup_coverage_gen_file(): + # delete the coverage file if it exists as part of test cleanup + yield + if os.path.exists(coverage_gen_file_path): # noqa: PTH110 + os.remove(coverage_gen_file_path) # noqa: PTH107 + + +def test_coverage_gen_report(cleanup_coverage_gen_file): # noqa: ARG001 + """ + Test coverage payload is correct for simple pytest example. Output of coverage run is below. + + Name Stmts Miss Branch BrPart Cover + --------------------------------------------------- + __init__.py 0 0 0 0 100% + reverse.py 13 3 8 2 76% + test_reverse.py 11 0 0 0 100% + --------------------------------------------------- + TOTAL 24 3 8 2 84% + + """ + args = ["--cov-report=json"] + env_add = {"COVERAGE_ENABLED": "True"} + cov_folder_path = TEST_DATA_PATH / "coverage_gen" + print("cov_folder_path", cov_folder_path) + actual = runner_with_cwd_env(args, cov_folder_path, env_add) + assert actual + cov = actual[-1] + assert cov + results = cov["result"] + assert results + assert len(results) == 3 + focal_function_coverage = results.get(os.fspath(TEST_DATA_PATH / "coverage_gen" / "reverse.py")) + assert focal_function_coverage + assert focal_function_coverage.get("lines_covered") is not None + assert focal_function_coverage.get("lines_missed") is not None + assert set(focal_function_coverage.get("lines_covered")) == {4, 5, 7, 9, 10, 11, 12, 13, 14, 17} + assert set(focal_function_coverage.get("lines_missed")) == {18, 19, 6} + coverage_version = Version(coverage.__version__) + # only include check for branches if the version is >= 7.7.0 + if coverage_version >= Version("7.7.0"): + assert focal_function_coverage.get("executed_branches") == 4 + assert focal_function_coverage.get("total_branches") == 6 + # assert that the coverage file was created at the right path + assert os.path.exists(coverage_gen_file_path) # noqa: PTH110 + + +def test_coverage_w_omit_config(): + """ + Test the coverage report generation with omit configuration. + + folder structure of coverage_w_config + ├── coverage_w_config + │ ├── test_ignore.py + │ ├── test_ran.py + │ └── pyproject.toml + │ ├── tests + │ │ └── test_disregard.py + + pyproject.toml file with the following content: + [tool.coverage.report] + omit = [ + "test_ignore.py", + "tests/*.py" (this will ignore the coverage in the file tests/test_disregard.py) + ] + + + Assertions: + - The coverage report is generated. + - The coverage report contains results. + - Only one file is reported in the coverage results. + """ + env_add = {"COVERAGE_ENABLED": "True"} + cov_folder_path = TEST_DATA_PATH / "coverage_w_config" + print("cov_folder_path", cov_folder_path) + actual = runner_with_cwd_env([], cov_folder_path, env_add) + assert actual + print("actual", json.dumps(actual, indent=2)) + cov = actual[-1] + assert cov + results = cov["result"] + assert results + # assert one file is reported and one file (as specified in pyproject.toml) is omitted + assert len(results) == 1 + + +def test_pytest_cov_manual_plugin_loading(): + """ + Test that pytest-cov is detected when loaded manually via -p pytest_cov.plugin. + + This test verifies the fix for issue #25590, where pytest-cov detection failed + when using --disable-plugin-autoload with -p pytest_cov.plugin. The plugin is + registered under its module name (pytest_cov.plugin) instead of entry point name + (pytest_cov) in this scenario. + """ + args = ["--collect-only"] + env_add = {"COVERAGE_ENABLED": "True", "_PYTEST_MANUAL_PLUGIN_LOAD": "True"} + cov_folder_path = TEST_DATA_PATH / "coverage_gen" + + # Should NOT raise VSCodePytestError about pytest-cov not being installed + actual = runner_with_cwd_env(args, cov_folder_path, env_add) + assert actual is not None + # Verify discovery succeeded (status != "error") + assert actual[0].get("status") != "error" diff --git a/python_files/tests/pytestadapter/test_discovery.py b/python_files/tests/pytestadapter/test_discovery.py new file mode 100644 index 000000000000..cf777399fed9 --- /dev/null +++ b/python_files/tests/pytestadapter/test_discovery.py @@ -0,0 +1,482 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import json +import os +import sys +from typing import Any, Dict, List, Optional + +import pytest + +from tests.tree_comparison_helper import is_same_tree + +from . import expected_discovery_test_output, helpers + + +def test_import_error(): + """Test pytest discovery on a file that has a pytest marker but does not import pytest. + + Copies the contents of a .txt file to a .py file in the temporary directory + to then run pytest discovery on. + + The json should still be returned but the errors list should be present. + + Keyword arguments: + tmp_path -- pytest fixture that creates a temporary directory. + """ + file_path = helpers.TEST_DATA_PATH / "error_pytest_import.txt" + with helpers.text_to_python_file(file_path) as p: + actual: Optional[List[Dict[str, Any]]] = helpers.runner(["--collect-only", os.fspath(p)]) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + pytest.fail(f"{error_content} is None or not a list, str, or tuple") + + +def test_syntax_error(tmp_path): # noqa: ARG001 + """Test pytest discovery on a file that has a syntax error. + + Copies the contents of a .txt file to a .py file in the temporary directory + to then run pytest discovery on. + + The json should still be returned but the errors list should be present. + + Keyword arguments: + tmp_path -- pytest fixture that creates a temporary directory. + """ + # Saving some files as .txt to avoid that file displaying a syntax error for + # the extension as a whole. Instead, rename it before running this test + # in order to test the error handling. + file_path = helpers.TEST_DATA_PATH / "error_syntax_discovery.txt" + with helpers.text_to_python_file(file_path) as p: + actual = helpers.runner(["--collect-only", os.fspath(p)]) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + pytest.fail(f"{error_content} is None or not a list, str, or tuple") + + +def test_parameterized_error_collect(): + """Tests pytest discovery on specific file that incorrectly uses parametrize. + + The json should still be returned but the errors list should be present. + """ + file_path_str = "error_parametrize_discovery.py" + actual = helpers.runner(["--collect-only", file_path_str]) + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "error" + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) + + # Ensure that 'error' is a list and then check its length + error_content = actual_item.get("error") + if error_content is not None and isinstance( + error_content, (list, tuple, str) + ): # You can add other types if needed + assert len(error_content) == 2 + else: + pytest.fail(f"{error_content} is None or not a list, str, or tuple") + + +@pytest.mark.parametrize( + ("file", "expected_const"), + [ + ( + "test_param_span_class.py", + expected_discovery_test_output.test_param_span_class_expected_output, + ), + ( + "test_multi_class_nest.py", + expected_discovery_test_output.nested_classes_expected_test_output, + ), + ( + "same_function_new_class_param.py", + expected_discovery_test_output.same_function_new_class_param_expected_output, + ), + ( + "unittest_skiptest_file_level.py", + expected_discovery_test_output.unittest_skip_file_level_expected_output, + ), + ( + "param_same_name", + expected_discovery_test_output.param_same_name_expected_output, + ), + ( + "parametrize_tests.py", + expected_discovery_test_output.parametrize_tests_expected_output, + ), + ( + "empty_discovery.py", + expected_discovery_test_output.empty_discovery_pytest_expected_output, + ), + ( + "simple_pytest.py", + expected_discovery_test_output.simple_discovery_pytest_expected_output, + ), + ( + "unittest_pytest_same_file.py", + expected_discovery_test_output.unit_pytest_same_file_discovery_expected_output, + ), + ( + "unittest_folder", + expected_discovery_test_output.unittest_folder_discovery_expected_output, + ), + ( + "dual_level_nested_folder", + expected_discovery_test_output.dual_level_nested_folder_expected_output, + ), + ( + "folder_a", + expected_discovery_test_output.double_nested_folder_expected_output, + ), + ( + "text_docstring.txt", + expected_discovery_test_output.doctest_pytest_expected_output, + ), + ( + "pytest_describe_plugin" + os.path.sep + "describe_only.py", + expected_discovery_test_output.expected_describe_only_output, + ), + ( + "pytest_describe_plugin" + os.path.sep + "nested_describe.py", + expected_discovery_test_output.expected_nested_describe_output, + ), + ], +) +def test_pytest_collect(file, expected_const): + """Test to test pytest discovery on a variety of test files/ folder structures. + + Uses variables from expected_discovery_test_output.py to store the expected + dictionary return. Only handles discovery and therefore already contains the arg + --collect-only. All test discovery will succeed, be in the correct cwd, and match + expected test output. + + Keyword arguments: + file -- a string with the file or folder to run pytest discovery on. + expected_const -- the expected output from running pytest discovery on the file. + """ + actual = helpers.runner( + [ + os.fspath(helpers.TEST_DATA_PATH / file), + "--collect-only", + ] + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) + assert is_same_tree( + actual_item.get("tests"), + expected_const, + ["id_", "lineno", "name", "runID"], + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_const, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="See https://stackoverflow.com/questions/32877260/privlege-error-trying-to-create-symlink-using-python-on-windows-10", +) +def test_symlink_root_dir(): + """Test to test pytest discovery with the command line arg --rootdir specified as a symlink path. + + Discovery should succeed and testids should be relative to the symlinked root directory. + """ + with helpers.create_symlink(helpers.TEST_DATA_PATH, "root", "symlink_folder") as ( + source, + destination, + ): + assert destination.is_symlink() + + # Run pytest with the cwd being the resolved symlink path (as it will be when we run the subprocess from node). + actual = helpers.runner_with_cwd( + ["--collect-only", f"--rootdir={os.fspath(destination)}"], source + ) + expected = expected_discovery_test_output.symlink_expected_discovery_output + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + try: + # Check if all requirements + assert all(item in actual_item for item in ("status", "cwd", "error")), ( + "Required keys are missing" + ) + assert actual_item.get("status") == "success", "Status is not 'success'" + assert actual_item.get("cwd") == os.fspath(destination), ( + f"CWD does not match: {os.fspath(destination)}" + ) + assert actual_item.get("tests") == expected, "Tests do not match expected value" + except AssertionError as e: + # Print the actual_item in JSON format if an assertion fails + print(json.dumps(actual_item, indent=4)) + pytest.fail(str(e)) + + +def test_pytest_root_dir(): + """Test to test pytest discovery with the command line arg --rootdir specified to be a subfolder of the workspace root. + + Discovery should succeed and testids should be relative to workspace root. + """ + rd = f"--rootdir={helpers.TEST_DATA_PATH / 'root' / 'tests'}" + actual = helpers.runner_with_cwd( + [ + "--collect-only", + rd, + ], + helpers.TEST_DATA_PATH / "root", + ) + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH / "root") + assert is_same_tree( + actual_item.get("tests"), + expected_discovery_test_output.root_with_config_expected_output, + ["id_", "lineno", "name", "runID"], + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.root_with_config_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) + + +def test_pytest_config_file(): + """Test to test pytest discovery with the command line arg -c with a specified config file which changes the workspace root. + + Discovery should succeed and testids should be relative to workspace root. + """ + actual = helpers.runner_with_cwd( + [ + "--collect-only", + "tests/", + ], + helpers.TEST_DATA_PATH / "root", + ) + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH / "root") + assert is_same_tree( + actual_item.get("tests"), + expected_discovery_test_output.root_with_config_expected_output, + ["id_", "lineno", "name", "runID"], + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.root_with_config_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) + + +def test_config_sub_folder(): + """Here the session node will be a subfolder of the workspace root and the test are in another subfolder. + + This tests checks to see if test node path are under the session node and if so the + session node is correctly updated to the common path. + """ + folder_path = helpers.TEST_DATA_PATH / "config_sub_folder" + actual = helpers.runner_with_cwd( + [ + "--collect-only", + "-c=config/pytest.ini", + "--rootdir=config/", + "-vv", + ], + folder_path, + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH / "config_sub_folder") + assert actual_item.get("tests") is not None + if actual_item.get("tests") is not None: + tests: Any = actual_item.get("tests") + assert tests.get("name") == "config_sub_folder" + + +@pytest.mark.parametrize( + ("file", "expected_const", "extra_arg"), + [ + ( + "folder_with_script", + expected_discovery_test_output.ruff_test_expected_output, + "--ruff", + ), + ( + "2496-black-formatter", + expected_discovery_test_output.black_formatter_expected_output, + "--black", + ), + ], +) +def test_plugin_collect(file, expected_const, extra_arg): + """Test pytest discovery on a folder with a plugin argument (e.g., --ruff, --black). + + Uses variables from expected_discovery_test_output.py to store the expected + dictionary return. Only handles discovery and therefore already contains the arg + --collect-only. All test discovery will succeed, be in the correct cwd, and match + expected test output. + + Keyword arguments: + file -- a string with the file or folder to run pytest discovery on. + expected_const -- the expected output from running pytest discovery on the file. + extra_arg -- the extra plugin argument to pass (e.g., --ruff, --black) + """ + file_path = helpers.TEST_DATA_PATH / file + actual = helpers.runner( + [os.fspath(file_path), "--collect-only", extra_arg], + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) + assert actual_item.get("cwd") == os.fspath(helpers.TEST_DATA_PATH) + assert is_same_tree( + actual_item.get("tests"), + expected_const, + ["id_", "lineno", "name", "runID"], + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_const, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) + + +def test_project_root_path_env_var(): + """Test pytest discovery with PROJECT_ROOT_PATH environment variable set. + + This simulates project-based testing where the test tree root should be + the project root (PROJECT_ROOT_PATH) rather than the workspace cwd. + + When PROJECT_ROOT_PATH is set: + - The test tree root (name, path, id_) should match PROJECT_ROOT_PATH + - The cwd in the response should match PROJECT_ROOT_PATH + - Test files should be direct children of the root (not nested under a subfolder) + """ + # Use unittest_folder as our "project" subdirectory + project_path = helpers.TEST_DATA_PATH / "unittest_folder" + + actual = helpers.runner_with_cwd_env( + [os.fspath(project_path), "--collect-only"], + helpers.TEST_DATA_PATH, # cwd is parent of project + {"PROJECT_ROOT_PATH": os.fspath(project_path)}, # Set project root + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + + assert all(item in actual_item for item in ("status", "cwd", "error")) + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) + # cwd in response should be PROJECT_ROOT_PATH + assert actual_item.get("cwd") == os.fspath(project_path), ( + f"Expected cwd '{os.fspath(project_path)}', got '{actual_item.get('cwd')}'" + ) + assert is_same_tree( + actual_item.get("tests"), + expected_discovery_test_output.project_root_unittest_folder_expected_output, + ["id_", "lineno", "name", "runID"], + ), ( + f"Tests tree does not match expected value. \n Expected: {json.dumps(expected_discovery_test_output.project_root_unittest_folder_expected_output, indent=4)}. \n Actual: {json.dumps(actual_item.get('tests'), indent=4)}" + ) + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="Symlinks require elevated privileges on Windows", +) +def test_symlink_with_project_root_path(): + """Test pytest discovery with both symlink and PROJECT_ROOT_PATH set. + + This tests the combination of: + 1. A symlinked test directory (--rootdir points to symlink) + 2. PROJECT_ROOT_PATH set to the symlink path + + This simulates project-based testing where the project root is a symlink, + ensuring test IDs and paths are correctly resolved through the symlink. + """ + with helpers.create_symlink(helpers.TEST_DATA_PATH, "root", "symlink_folder") as ( + source, + destination, + ): + assert destination.is_symlink() + + # Run pytest with: + # - cwd being the resolved symlink path (simulating subprocess from node) + # - PROJECT_ROOT_PATH set to the symlink destination + actual = helpers.runner_with_cwd_env( + ["--collect-only", f"--rootdir={os.fspath(destination)}"], + source, # cwd is the resolved (non-symlink) path + {"PROJECT_ROOT_PATH": os.fspath(destination)}, # Project root is the symlink + ) + + expected = expected_discovery_test_output.symlink_expected_discovery_output + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + try: + assert all(item in actual_item for item in ("status", "cwd", "error")), ( + "Required keys are missing" + ) + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) + # cwd should be the PROJECT_ROOT_PATH (the symlink destination) + assert actual_item.get("cwd") == os.fspath(destination), ( + f"CWD does not match symlink path: expected {os.fspath(destination)}, got {actual_item.get('cwd')}" + ) + assert actual_item.get("tests") == expected, "Tests do not match expected value" + except AssertionError as e: + # Print the actual_item in JSON format if an assertion fails + print(json.dumps(actual_item, indent=4)) + pytest.fail(str(e)) diff --git a/python_files/tests/pytestadapter/test_execution.py b/python_files/tests/pytestadapter/test_execution.py new file mode 100644 index 000000000000..95a66e0e7b87 --- /dev/null +++ b/python_files/tests/pytestadapter/test_execution.py @@ -0,0 +1,274 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import json +import os +import pathlib +import sys +from typing import Any, Dict, List + +import pytest + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) + +from tests.pytestadapter import expected_execution_test_output # noqa: E402 + +from .helpers import ( # noqa: E402 + TEST_DATA_PATH, + create_symlink, + get_absolute_test_id, + runner, + runner_with_cwd, +) + + +def test_config_file(): + """Test pytest execution when a config file is specified.""" + args = [ + "-c", + "tests/pytest.ini", + str(TEST_DATA_PATH / "root" / "tests" / "test_a.py::test_a_function"), + ] + new_cwd = TEST_DATA_PATH / "root" + actual = runner_with_cwd(args, new_cwd) + expected_const = expected_execution_test_output.config_file_pytest_expected_execution_output + assert actual + actual_list: List[Dict[str, Any]] = actual + assert len(actual_list) == len(expected_const) + actual_result_dict = {} + if actual_list is not None: + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "result")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(new_cwd) + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const + + +def test_rootdir_specified(): + """Test pytest execution when a --rootdir is specified.""" + rd = f"--rootdir={TEST_DATA_PATH / 'root' / 'tests'}" + args = [rd, "tests/test_a.py::test_a_function"] + new_cwd = TEST_DATA_PATH / "root" + actual = runner_with_cwd(args, new_cwd) + expected_const = expected_execution_test_output.config_file_pytest_expected_execution_output + assert actual + actual_list: List[Dict[str, Dict[str, Any]]] = actual + assert len(actual_list) == len(expected_const) + actual_result_dict = {} + if actual_list is not None: + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "result")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(new_cwd) + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const + + +@pytest.mark.parametrize( + ("test_ids", "expected_const"), + [ + pytest.param( + [ + "test_env_vars.py::test_clear_env", + "test_env_vars.py::test_check_env", + ], + expected_execution_test_output.safe_clear_env_vars_expected_execution_output, + id="safe_clear_env_vars", + ), + pytest.param( + [ + "skip_tests.py::test_something", + "skip_tests.py::test_another_thing", + "skip_tests.py::test_decorator_thing", + "skip_tests.py::test_decorator_thing_2", + "skip_tests.py::TestClass::test_class_function_a", + "skip_tests.py::TestClass::test_class_function_b", + ], + expected_execution_test_output.skip_tests_execution_expected_output, + id="skip_tests_execution", + ), + pytest.param( + ["error_raise_exception.py::TestSomething::test_a"], + expected_execution_test_output.error_raised_exception_execution_expected_output, + id="error_raised_exception", + ), + pytest.param( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_negative_numbers", + ], + expected_execution_test_output.uf_execution_expected_output, + id="unittest_multiple_files", + ), + pytest.param( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "unittest_folder/test_add.py::TestAddFunction::test_add_negative_numbers", + ], + expected_execution_test_output.uf_single_file_expected_output, + id="unittest_single_file", + ), + pytest.param( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + ], + expected_execution_test_output.uf_single_method_execution_expected_output, + id="unittest_single_method", + ), + pytest.param( + [ + "unittest_folder/test_add.py::TestAddFunction::test_add_positive_numbers", + "unittest_folder/test_subtract.py::TestSubtractFunction::test_subtract_positive_numbers", + ], + expected_execution_test_output.uf_non_adjacent_tests_execution_expected_output, + id="unittest_non_adjacent_tests", + ), + pytest.param( + [ + "unittest_pytest_same_file.py::TestExample::test_true_unittest", + "unittest_pytest_same_file.py::test_true_pytest", + ], + expected_execution_test_output.unit_pytest_same_file_execution_expected_output, + id="unittest_pytest_same_file", + ), + pytest.param( + [ + "dual_level_nested_folder/test_top_folder.py::test_top_function_t", + "dual_level_nested_folder/test_top_folder.py::test_top_function_f", + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_t", + "dual_level_nested_folder/nested_folder_one/test_bottom_folder.py::test_bottom_function_f", + ], + expected_execution_test_output.dual_level_nested_folder_execution_expected_output, + id="dual_level_nested_folder", + ), + pytest.param( + ["folder_a/folder_b/folder_a/test_nest.py::test_function"], + expected_execution_test_output.double_nested_folder_expected_execution_output, + id="double_nested_folder", + ), + pytest.param( + [ + "parametrize_tests.py::TestClass::test_adding[3+5-8]", + "parametrize_tests.py::TestClass::test_adding[2+4-6]", + "parametrize_tests.py::TestClass::test_adding[6+9-16]", + ], + expected_execution_test_output.parametrize_tests_expected_execution_output, + id="parametrize_tests", + ), + pytest.param( + [ + "parametrize_tests.py::TestClass::test_adding[3+5-8]", + ], + expected_execution_test_output.single_parametrize_tests_expected_execution_output, + id="single_parametrize_test", + ), + pytest.param( + [ + "text_docstring.txt::text_docstring.txt", + ], + expected_execution_test_output.doctest_pytest_expected_execution_output, + id="doctest_pytest", + ), + pytest.param( + ["test_logging.py::test_logging2", "test_logging.py::test_logging"], + expected_execution_test_output.logging_test_expected_execution_output, + id="logging_tests", + ), + pytest.param( + [ + "pytest_describe_plugin/describe_only.py::describe_A::test_1", + "pytest_describe_plugin/describe_only.py::describe_A::test_2", + ], + expected_execution_test_output.describe_only_expected_execution_output, + id="describe_only", + ), + pytest.param( + [ + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::add_empty", + "pytest_describe_plugin/nested_describe.py::describe_list::describe_append::remove_empty", + "pytest_describe_plugin/nested_describe.py::describe_list::describe_remove::removes", + ], + expected_execution_test_output.nested_describe_expected_execution_output, + id="nested_describe_plugin", + ), + pytest.param( + ["skip_test_fixture.py::test_docker_client"], + expected_execution_test_output.skip_test_fixture_execution_expected_output, + id="skip_test_fixture", + ), + ], +) +def test_pytest_execution(test_ids, expected_const): + """ + Test that pytest discovery works as expected where run pytest is always successful, but the actual test results are both successes and failures. + + Keyword arguments: + test_ids -- an array of test_ids to run. + expected_const -- a dictionary of the expected output from running pytest discovery on the files. + """ + args = test_ids + actual = runner(args) + assert actual + actual_list: List[Dict[str, Dict[str, Any]]] = actual + assert len(actual_list) == len(expected_const) + actual_result_dict = {} + if actual_list is not None: + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "result")) + assert actual_item.get("status") == "success" + assert actual_item.get("cwd") == os.fspath(TEST_DATA_PATH) + actual_result_dict.update(actual_item["result"]) + for key in actual_result_dict: + if ( + actual_result_dict[key]["outcome"] == "failure" + or actual_result_dict[key]["outcome"] == "error" + ): + actual_result_dict[key]["message"] = "ERROR MESSAGE" + if actual_result_dict[key]["traceback"] is not None: + actual_result_dict[key]["traceback"] = "TRACEBACK" + assert actual_result_dict == expected_const + + +def test_symlink_run(): + """Test to test pytest discovery with the command line arg --rootdir specified as a symlink path. + + Discovery should succeed and testids should be relative to the symlinked root directory. + """ + with create_symlink(TEST_DATA_PATH, "root", "symlink_folder") as ( + source, + destination, + ): + assert destination.is_symlink() + test_a_path = TEST_DATA_PATH / "symlink_folder" / "tests" / "test_a.py" + test_a_id = get_absolute_test_id( + "tests/test_a.py::test_a_function", + test_a_path, + ) + + # Run pytest with the cwd being the resolved symlink path (as it will be when we run the subprocess from node). + actual = runner_with_cwd([f"--rootdir={os.fspath(destination)}", test_a_id], source) + + expected_const = expected_execution_test_output.symlink_run_expected_execution_output + assert actual + actual_list: List[Dict[str, Any]] = actual + if actual_list is not None: + actual_item = actual_list.pop(0) + try: + # Check if all requirements + assert all(item in actual_item for item in ("status", "cwd", "result")), ( + "Required keys are missing" + ) + assert actual_item.get("status") == "success", "Status is not 'success'" + assert actual_item.get("cwd") == os.fspath(destination), ( + f"CWD does not match: {os.fspath(destination)}" + ) + actual_result_dict = {} + actual_result_dict.update(actual_item["result"]) + assert actual_result_dict == expected_const + except AssertionError as e: + # Print the actual_item in JSON format if an assertion fails + print(json.dumps(actual_item, indent=4)) + pytest.fail(str(e)) diff --git a/python_files/tests/pytestadapter/test_utils.py b/python_files/tests/pytestadapter/test_utils.py new file mode 100644 index 000000000000..70201db7d097 --- /dev/null +++ b/python_files/tests/pytestadapter/test_utils.py @@ -0,0 +1,57 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys +import tempfile + +from .helpers import ( + TEST_DATA_PATH, +) + +script_dir = pathlib.Path(__file__).parent.parent.parent +sys.path.append(os.fspath(script_dir)) +from vscode_pytest import cached_fsdecode, has_symlink_parent # noqa: E402 + + +def test_has_symlink_parent_with_symlink(): + # Create a temporary directory and a file in it + with tempfile.TemporaryDirectory() as temp_dir: + file_path = pathlib.Path(temp_dir) / "file" + file_path.touch() + + # Create a symbolic link to the temporary directory + symlink_path = pathlib.Path(temp_dir) / "symlink" + symlink_path.symlink_to(temp_dir) + + # Check that has_symlink_parent correctly identifies the symbolic link + assert has_symlink_parent(symlink_path / "file") + + +def test_has_symlink_parent_without_symlink(): + folder_path = TEST_DATA_PATH / "unittest_folder" / "test_add.py" + # Check that has_symlink_parent correctly identifies that there are no symbolic links + assert not has_symlink_parent(folder_path) + + +def test_cached_fsdecode(): + """Test that cached_fsdecode correctly caches path-to-string conversions.""" + # Create a test path + test_path = TEST_DATA_PATH / "simple_pytest.py" + + # First call should compute and cache + result1 = cached_fsdecode(test_path) + assert result1 == os.fspath(test_path) + assert isinstance(result1, str) + + # Second call should return cached value (same object) + result2 = cached_fsdecode(test_path) + assert result2 == result1 + assert result2 is result1 # Should be the same object from cache + + # Different path should be cached independently + test_path2 = TEST_DATA_PATH / "parametrize_tests.py" + result3 = cached_fsdecode(test_path2) + assert result3 == os.fspath(test_path2) + assert result3 != result1 diff --git a/python_files/tests/run_all.py b/python_files/tests/run_all.py new file mode 100644 index 000000000000..3edb3cd3440c --- /dev/null +++ b/python_files/tests/run_all.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# Replace the "." entry. +import os +import pathlib +import sys + +sys.path[0] = os.fsdecode(pathlib.Path(__file__).parent.parent) + +from tests.__main__ import main, parse_args # noqa: E402 + +if __name__ == "__main__": + mainkwargs, pytestargs = parse_args() + ec = main(pytestargs, **mainkwargs) + sys.exit(ec) diff --git a/python_files/tests/test_create_conda.py b/python_files/tests/test_create_conda.py new file mode 100644 index 000000000000..82daafbea9dc --- /dev/null +++ b/python_files/tests/test_create_conda.py @@ -0,0 +1,71 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import importlib +import sys + +import pytest + +import create_conda + + +@pytest.mark.parametrize("env_exists", [True, False]) +@pytest.mark.parametrize("git_ignore", [True, False]) +@pytest.mark.parametrize("install", [True, False]) +@pytest.mark.parametrize("python", [True, False]) +def test_create_env(env_exists, git_ignore, install, python): + importlib.reload(create_conda) + create_conda.conda_env_exists = lambda _n: env_exists + + install_packages_called = False + + def install_packages(_name): + nonlocal install_packages_called + install_packages_called = True + + create_conda.install_packages = install_packages + + run_process_called = False + + def run_process(args, error_message): + nonlocal run_process_called + run_process_called = True + version = "12345" if python else f"{sys.version_info.major}.{sys.version_info.minor}" + if not env_exists: + assert args == [ + sys.executable, + "-m", + "conda", + "create", + "--yes", + "--prefix", + create_conda.CONDA_ENV_NAME, + f"python={version}", + ] + assert error_message == "CREATE_CONDA.ENV_FAILED_CREATION" + + create_conda.run_process = run_process + + add_gitignore_called = False + + def add_gitignore(_name): + nonlocal add_gitignore_called + add_gitignore_called = True + + create_conda.add_gitignore = add_gitignore + + args = [] + if git_ignore: + args.append("--git-ignore") + if install: + args.append("--install") + if python: + args.extend(["--python", "12345"]) + create_conda.main(args) + assert install_packages_called == install + + # run_process is called when the venv does not exist + assert run_process_called != env_exists + + # add_gitignore is called when new venv is created and git_ignore is True + assert add_gitignore_called == (not env_exists and git_ignore) diff --git a/python_files/tests/test_create_microvenv.py b/python_files/tests/test_create_microvenv.py new file mode 100644 index 000000000000..e5d4e68802e9 --- /dev/null +++ b/python_files/tests/test_create_microvenv.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import importlib +import os +import sys + +import create_microvenv + + +def test_create_microvenv(): + importlib.reload(create_microvenv) + run_process_called = False + + def run_process(args, error_message): + nonlocal run_process_called + run_process_called = True + assert args == [ + sys.executable, + os.fspath(create_microvenv.LIB_ROOT / "microvenv.py"), + create_microvenv.VENV_NAME, + ] + assert error_message == "CREATE_MICROVENV.MICROVENV_FAILED_CREATION" + + create_microvenv.run_process = run_process + + create_microvenv.main() + assert run_process_called is True diff --git a/python_files/tests/test_create_venv.py b/python_files/tests/test_create_venv.py new file mode 100644 index 000000000000..6308934d71a0 --- /dev/null +++ b/python_files/tests/test_create_venv.py @@ -0,0 +1,300 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import contextlib +import importlib +import io +import json +import os +import sys + +import pytest + +import create_venv + + +@pytest.mark.skipif(sys.platform == "win32", reason="Windows does not have micro venv fallback.") +def test_venv_not_installed_unix(): + importlib.reload(create_venv) + create_venv.is_installed = lambda module: module != "venv" + run_process_called = False + + def run_process(args, error_message): + nonlocal run_process_called + microvenv_path = os.fspath(create_venv.MICROVENV_SCRIPT_PATH) + if microvenv_path in args: + run_process_called = True + assert args == [ + sys.executable, + microvenv_path, + "--name", + ".test_venv", + ] + assert error_message == "CREATE_VENV.MICROVENV_FAILED_CREATION" + + create_venv.run_process = run_process + + create_venv.main(["--name", ".test_venv"]) + + # run_process is called when the venv does not exist + assert run_process_called is True + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows does not have microvenv fallback.") +def test_venv_not_installed_windows(): + importlib.reload(create_venv) + create_venv.is_installed = lambda module: module != "venv" + with pytest.raises(create_venv.VenvError) as e: + create_venv.main() + assert str(e.value) == "CREATE_VENV.VENV_NOT_FOUND" + + +@pytest.mark.parametrize("env_exists", ["hasEnv", "noEnv"]) +@pytest.mark.parametrize("git_ignore", ["useGitIgnore", "skipGitIgnore", "gitIgnoreExists"]) +@pytest.mark.parametrize("install", ["requirements", "toml", "skipInstall"]) +def test_create_env(env_exists, git_ignore, install): + importlib.reload(create_venv) + create_venv.is_installed = lambda _x: True + create_venv.venv_exists = lambda _n: env_exists == "hasEnv" + create_venv.upgrade_pip = lambda _x: None + create_venv.is_file = lambda _x: git_ignore == "gitIgnoreExists" + + install_packages_called = False + + def install_packages(_env, _name): + nonlocal install_packages_called + install_packages_called = True + + create_venv.install_requirements = install_packages + create_venv.install_toml = install_packages + + run_process_called = False + + def run_process(args, error_message): + nonlocal run_process_called + run_process_called = True + if env_exists == "noEnv": + assert args == [sys.executable, "-m", "venv", create_venv.VENV_NAME] + assert error_message == "CREATE_VENV.VENV_FAILED_CREATION" + + create_venv.run_process = run_process + + add_gitignore_called = False + + def add_gitignore(_name): + nonlocal add_gitignore_called + add_gitignore_called = True + if not create_venv.is_file(_name): + create_venv.create_gitignore(_name) + + create_venv.add_gitignore = add_gitignore + + create_gitignore_called = False + + def create_gitignore(_p): + nonlocal create_gitignore_called + create_gitignore_called = True + + create_venv.create_gitignore = create_gitignore + + args = [] + if git_ignore == "useGitIgnore": + args += ["--git-ignore"] + if install == "requirements": + args += ["--requirements", "requirements-for-test.txt"] + elif install == "toml": + args += ["--toml", "pyproject.toml", "--extras", "test"] + + create_venv.main(args) + assert install_packages_called == (install != "skipInstall") + + # run_process is called when the venv does not exist + assert run_process_called == (env_exists == "noEnv") + + # add_gitignore is called when new venv is created and git_ignore is True + assert add_gitignore_called == ((env_exists == "noEnv") and (git_ignore == "useGitIgnore")) + + assert create_gitignore_called == (add_gitignore_called and (git_ignore != "gitIgnoreExists")) + + +@pytest.mark.parametrize("install_type", ["requirements", "pyproject", "both"]) +def test_install_packages(install_type): + importlib.reload(create_venv) + create_venv.is_installed = lambda _x: True + create_venv.file_exists = lambda x: install_type in str(x) + + pip_upgraded = False + installing = None + + order = [] + + def run_process(args, error_message): + nonlocal pip_upgraded, installing, order + if args[1:] == ["-m", "pip", "install", "--upgrade", "pip"]: + pip_upgraded = True + assert error_message == "CREATE_VENV.UPGRADE_PIP_FAILED" + elif args[1:-1] == ["-m", "pip", "install", "-r"]: + installing = "requirements" + order += ["requirements"] + assert error_message == "CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS" + elif args[1:] == ["-m", "pip", "install", "-e", ".[test]"]: + installing = "pyproject" + order += ["pyproject"] + assert error_message == "CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT" + + create_venv.run_process = run_process + + if install_type == "requirements": + create_venv.main(["--requirements", "requirements-for-test.txt"]) + elif install_type == "pyproject": + create_venv.main(["--toml", "pyproject.toml", "--extras", "test"]) + elif install_type == "both": + create_venv.main( + [ + "--requirements", + "requirements-for-test.txt", + "--toml", + "pyproject.toml", + "--extras", + "test", + ] + ) + + assert pip_upgraded + if install_type == "both": + assert order == ["requirements", "pyproject"] + else: + assert installing == install_type + + +@pytest.mark.parametrize( + ("extras", "expected"), + [ + ([], ["-m", "pip", "install", "-e", "."]), + (["test"], ["-m", "pip", "install", "-e", ".[test]"]), + (["test", "doc"], ["-m", "pip", "install", "-e", ".[test,doc]"]), + ], +) +def test_toml_args(extras, expected): + importlib.reload(create_venv) + + actual = [] + + def run_process(args, error_message): # noqa: ARG001 + nonlocal actual + actual = args[1:] + + create_venv.run_process = run_process + + create_venv.install_toml(sys.executable, extras) + + assert actual == expected + + +@pytest.mark.parametrize( + ("extras", "expected"), + [ + ([], []), + ( + ["requirements/test.txt"], + [[sys.executable, "-m", "pip", "install", "-r", "requirements/test.txt"]], + ), + ( + ["requirements/test.txt", "requirements/doc.txt"], + [ + [sys.executable, "-m", "pip", "install", "-r", "requirements/test.txt"], + [sys.executable, "-m", "pip", "install", "-r", "requirements/doc.txt"], + ], + ), + ], +) +def test_requirements_args(extras, expected): + importlib.reload(create_venv) + + actual = [] + + def run_process(args, error_message): # noqa: ARG001 + nonlocal actual + actual.append(args) + + create_venv.run_process = run_process + + create_venv.install_requirements(sys.executable, extras) + + assert actual == expected + + +def test_create_venv_missing_pip(): + importlib.reload(create_venv) + create_venv.venv_exists = lambda _n: True + create_venv.is_installed = lambda module: module != "pip" + + download_pip_pyz_called = False + + def download_pip_pyz(name): + nonlocal download_pip_pyz_called + download_pip_pyz_called = True + assert name == create_venv.VENV_NAME + + create_venv.download_pip_pyz = download_pip_pyz + + run_process_called = False + + def run_process(args, error_message): + if "install" in args and "pip" in args: + nonlocal run_process_called + run_process_called = True + pip_pyz_path = os.fspath(create_venv.CWD / create_venv.VENV_NAME / "pip.pyz") + assert args[1:] == [pip_pyz_path, "install", "pip"] + assert error_message == "CREATE_VENV.INSTALL_PIP_FAILED" + + create_venv.run_process = run_process + create_venv.main([]) + + +@contextlib.contextmanager +def redirect_io(stream: str, new_stream): + """Redirect stdio streams to a custom stream.""" + old_stream = getattr(sys, stream) + setattr(sys, stream, new_stream) + yield + setattr(sys, stream, old_stream) + + +class CustomIO(io.TextIOWrapper): + """Custom stream object to replace stdio.""" + + name: str = "customio" + + def __init__(self, name: str, encoding="utf-8", newline=None): + self._buffer = io.BytesIO() + self._buffer.name = name + super().__init__(self._buffer, encoding=encoding, newline=newline) + + def close(self): + """Provide this close method which is used by some tools.""" + # This is intentionally empty. + + def get_value(self) -> str: + """Returns value from the buffer as string.""" + self.seek(0) + return self.read() + + +def test_requirements_from_stdin(): + importlib.reload(create_venv) + + cli_requirements = [f"cli-requirement{i}.txt" for i in range(3)] + args = argparse.Namespace() + args.__dict__.update({"stdin": True, "requirements": cli_requirements}) + + stdin_requirements = [f"stdin-requirement{i}.txt" for i in range(20)] + text = json.dumps({"requirements": stdin_requirements}) + str_input = CustomIO("<stdin>", encoding="utf-8", newline="\n") + with redirect_io("stdin", str_input): + str_input.write(text) + str_input.seek(0) + actual = create_venv.get_requirements_from_args(args) + + assert actual == stdin_requirements + cli_requirements diff --git a/python_files/tests/test_data/missing-deps.data b/python_files/tests/test_data/missing-deps.data new file mode 100644 index 000000000000..c8c911f218a8 --- /dev/null +++ b/python_files/tests/test_data/missing-deps.data @@ -0,0 +1,121 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# pip-compile --generate-hashes --resolver=backtracking requirements-test.in +# +flake8-csv==0.2.0 \ + --hash=sha256:246e07207fefbf8f80a59ff7e878f153635f562ebaf20cf796a2b00b1528ea9a \ + --hash=sha256:bf3ac6aecbaebe36a2c7d5d275f310996fcc33b7370cdd81feec04b79af2e07c + # via -r requirements-test.in +levenshtein==0.21.0 \ + --hash=sha256:01dd427cf72b4978b09558e3d36e3f92c8eef467e3eb4653c3fdccd8d70aaa08 \ + --hash=sha256:0236c8ff4648c50ebd81ac3692430d2241b134936ac9d86d7ca32ba6ab4a4e63 \ + --hash=sha256:023ca95c833ca548280e444e9a4c34fdecb3be3851e96af95bad290ae0c708b9 \ + --hash=sha256:024302c82d49fc1f1d044794997ef7aa9d01b509a9040e222480b64a01cd4b80 \ + --hash=sha256:04046878a57129da4e2352c032df7c1fceaa54870916d12772cad505ef998290 \ + --hash=sha256:04850a0719e503014acb3fee6d4ec7d7f170a2c7375ffbc5833c7256b7cd10ee \ + --hash=sha256:0cc3679978cd0250bf002963cf2e08855b93f70fa0fc9f74956115c343983fbb \ + --hash=sha256:0f42b8dba2cce257cd34efd1ce9678d06f3248cb0bb2a92a5db8402e1e4a6f30 \ + --hash=sha256:13e8a5b1b58de49befea555bb913dc394614f2d3553bc5b86bc672c69ef1a85a \ + --hash=sha256:1f19fe25ea0dd845d0f48505e8947f6080728e10b7642ba0dad34e9b48c81130 \ + --hash=sha256:1fde464f937878e6f5c30c234b95ce2cb969331a175b3089367e077113428062 \ + --hash=sha256:2290732763e3b75979888364b26acce79d72b8677441b5762a4e97b3630cc3d9 \ + --hash=sha256:24843f28cbbdcbcfc18b08e7d3409dbaad7896fb7113442592fa978590a7bbf0 \ + --hash=sha256:25576ad9c337ecb342306fe87166b54b2f49e713d4ff592c752cc98e0046296e \ + --hash=sha256:26c6fb012538a245d78adea786d2cfe3c1506b835762c1c523a4ed6b9e08dc0b \ + --hash=sha256:31cb59d86a5f99147cd4a67ebced8d6df574b5d763dcb63c033a642e29568746 \ + --hash=sha256:32dfda2e64d0c50553e47d0ab2956413970f940253351c196827ad46f17916d5 \ + --hash=sha256:3305262cb85ff78ace9e2d8d2dfc029b34dc5f93aa2d24fd20b6ed723e2ad501 \ + --hash=sha256:37a99d858fa1d88b1a917b4059a186becd728534e5e889d583086482356b7ca1 \ + --hash=sha256:3c6858cfd84568bc1df3ad545553b5c27af6ed3346973e8f4b57d23c318cf8f4 \ + --hash=sha256:3e1723d515ab287b9b2c2e4a111894dc6b474f5d28826fff379647486cae98d2 \ + --hash=sha256:3e22d31375d5fea5797c9b7aa0f8cc36579c31dcf5754e9931ca86c27d9011f8 \ + --hash=sha256:426883be613d912495cf6ee2a776d2ab84aa6b3de5a8d82c43a994267ea6e0e3 \ + --hash=sha256:4357bf8146cbadb10016ad3a950bba16e042f79015362a575f966181d95b4bc7 \ + --hash=sha256:4515f9511cb91c66d254ee30154206aad76b57d8b25f64ba1402aad43efdb251 \ + --hash=sha256:457442911df185e28a32fd8b788b14ca22ab3a552256b556e7687173d5f18bc4 \ + --hash=sha256:46dab8c6e8fae563ca77acfaeb3824c4dd4b599996328b8a081b06f16befa6a0 \ + --hash=sha256:4b2156f32e46d16b74a055ccb4f64ee3c64399372a6aaf1ee98f6dccfadecee1 \ + --hash=sha256:4bbceef2caba4b2ae613b0e853a7aaab990c1a13bddb9054ba1328a84bccdbf7 \ + --hash=sha256:4c8eaaa6f0df2838437d1d8739629486b145f7a3405d3ef0874301a9f5bc7dcd \ + --hash=sha256:4dc79033140f82acaca40712a6d26ed190cc2dd403e104020a87c24f2771aa72 \ + --hash=sha256:4ec2ef9836a34a3bb009a81e5efe4d9d43515455fb5f182c5d2cf8ae61c79496 \ + --hash=sha256:5369827ace536c6df04e0e670d782999bc17bf9eb111e77435fdcdaecb10c2a3 \ + --hash=sha256:5378a8139ba61d7271c0f9350201259c11eb90bfed0ac45539c4aeaed3907230 \ + --hash=sha256:545635d9e857711d049dcdb0b8609fb707b34b032517376c531ca159fcd46265 \ + --hash=sha256:587ad51770de41eb491bea1bfb676abc7ff9a94dbec0e2bc51fc6a25abef99c4 \ + --hash=sha256:5cfbc4ed7ee2965e305bf81388fea377b795dabc82ee07f04f31d1fb8677a885 \ + --hash=sha256:5e748c2349719cb1bc90f802d9d7f07310633dcf166d468a5bd821f78ed17698 \ + --hash=sha256:608beb1683508c3cdbfff669c1c872ea02b47965e1bbb8a630de548e2490f96a \ + --hash=sha256:6338a47b6f8c7f1ee8b5636cc8b245ad2d1d0ee47f7bb6f33f38a522ef0219cc \ + --hash=sha256:668ea30b311944c643f866ce5e45edf346f05e920075c0056f2ba7f74dde6071 \ + --hash=sha256:66d303cd485710fe6d62108209219b7a695bdd10a722f4e86abdaf26f4bf2202 \ + --hash=sha256:6ebabcf982ae161534f8729d13fe05eebc977b497ac34936551f97cf8b07dd9e \ + --hash=sha256:6ede583155f24c8b2456a7720fbbfa5d9c1154ae04b4da3cf63368e2406ea099 \ + --hash=sha256:709a727f58d31a5ee1e5e83b247972fe55ef0014f6222256c9692c5efa471785 \ + --hash=sha256:742b785c93d16c63289902607219c200bd2b6077dafc788073c74337cae382fb \ + --hash=sha256:76d5d34a8e21de8073c66ae801f053520f946d499fa533fbba654712775f8132 \ + --hash=sha256:7bc550d0986ace95bde003b8a60e622449baf2bdf24d8412f7a50f401a289ec3 \ + --hash=sha256:7c2d67220867d640e36931b3d63b8349369b485d52cf6f4a2635bec8da92d678 \ + --hash=sha256:7ce3f14a8e006fb7e3fc7bab965ab7da5817f48fc48d25cf735fcec8f1d2e39a \ + --hash=sha256:7e40a4bac848c9a8883225f926cfa7b2bc9f651e989a8b7006cdb596edc7ac9b \ + --hash=sha256:80e67bd73a05592ecd52aede4afa8ea49575de70f9d5bfbe2c52ebd3541b20be \ + --hash=sha256:8446f8da38857482ec0cfd616fe5e7dcd3695fd323cc65f37366a9ff6a31c9cb \ + --hash=sha256:8476862a5c3150b8d63a7475563a4bff6dc50bbc0447894eb6b6a116ced0809d \ + --hash=sha256:84b55b732e311629a8308ad2778a0f9824e29e3c35987eb35610fc52eb6d4634 \ + --hash=sha256:88ccdc8dc20c16e8059ace00fb58d353346a04fd24c0733b009678b2554801d2 \ + --hash=sha256:8aa92b05156dfa2e248c3743670d5deb41a45b5789416d5fa31be009f4f043ab \ + --hash=sha256:8ac4ed77d3263eac7f9b6ed89d451644332aecd55cda921201e348803a1e5c57 \ + --hash=sha256:8bdbcd1570340b07549f71e8a5ba3f0a6d84408bf86c4051dc7b70a29ae342bb \ + --hash=sha256:8c031cbe3685b0343f5cc2dcf2172fd21b82f8ccc5c487179a895009bf0e4ea8 \ + --hash=sha256:8c27a5178ce322b56527a451185b4224217aa81955d9b0dad6f5a8de81ffe80f \ + --hash=sha256:8cf87a5e2962431d7260dd81dc1ca0697f61aad81036145d3666f4c0d514ce3a \ + --hash=sha256:8d4ba0df46bb41d660d77e7cc6b4d38c8d5b6f977d51c48ed1217db6a8474cde \ + --hash=sha256:8dd8ef4239b24fb1c9f0b536e48e55194d5966d351d349af23e67c9eb3875c68 \ + --hash=sha256:92bf2370b01d7a4862abf411f8f60f39f064cebebce176e3e9ee14e744db8288 \ + --hash=sha256:9485f2a5c88113410153256657072bc93b81bf5c8690d47e4cc3df58135dbadb \ + --hash=sha256:9ff1255c499fcb41ba37a578ad8c1b8dab5c44f78941b8e1c1d7fab5b5e831bc \ + --hash=sha256:a18c8e4d1aae3f9950797d049020c64a8a63cc8b4e43afcca91ec400bf6304c5 \ + --hash=sha256:a68b05614d25cc2a5fbcc4d2fd124be7668d075fd5ac3d82f292eec573157361 \ + --hash=sha256:a7adaabe07c5ceb6228332b9184f06eb9cda89c227d198a1b8a6f78c05b3c672 \ + --hash=sha256:aa39bb773915e4df330d311bb6c100a8613e265cc50d5b25b015c8db824e1c47 \ + --hash=sha256:ac8b6266799645827980ab1af4e0bfae209c1f747a10bdf6e5da96a6ebe511a2 \ + --hash=sha256:b0ba9723c7d67a61e160b3457259552f7d679d74aaa144b892eb68b7e2a5ebb6 \ + --hash=sha256:b167b32b3e336c5ec5e0212f025587f9248344ae6e73ed668270eba5c6a506e5 \ + --hash=sha256:b646ace5085a60d4f89b28c81301c9d9e8cd6a9bdda908181b2fa3dfac7fc10d \ + --hash=sha256:bd0bfa71b1441be359e99e77709885b79c22857bf9bb7f4e84c09e501f6c5fad \ + --hash=sha256:be038321695267a8faa5ae1b1a83deb3748827f0b6f72471e0beed36afcbd72a \ + --hash=sha256:be87998ffcbb5fb0c37a76d100f63b4811f48527192677da0ec3624b49ab8a64 \ + --hash=sha256:c270487d60b33102efea73be6dcd5835f3ddc3dc06e77499f0963df6cba2ec71 \ + --hash=sha256:c290a7211f1b4f87c300df4424cc46b7379cead3b6f37fa8d3e7e6c6212ccd39 \ + --hash=sha256:cc36ba40027b4f8821155c9e3e0afadffccdccbe955556039d1d1169dfc659c9 \ + --hash=sha256:ce7e76c6341abb498368d42b8081f2f45c245ac2a221af6a0394349d41302c08 \ + --hash=sha256:cefd5a668f6d7af1279aca10104b43882fdd83f9bdc68933ba5429257a628abe \ + --hash=sha256:cf2dee0f8c71598f8be51e3feceb9142ac01576277b9e691e25740987761c86e \ + --hash=sha256:d23c647b03acbb5783f9bdfd51cfa5365d51f7df9f4029717a35eff5cc32bbcc \ + --hash=sha256:d647f1e0c30c7a73f70f4de7376ed7dafc2b856b67fe480d32a81af133edbaeb \ + --hash=sha256:d932cb21e40beb93cfc8973de7f25fbf25ba4a07d1dccac3b9ba977164cf9887 \ + --hash=sha256:db7567997ffbc2feb999e30002a92461a76f17a596a142bdb463b5f7037f160c \ + --hash=sha256:de2dfd6498454c7d89036d56a53c0a01fd9bcf1c2970253e469b5e8bb938b69f \ + --hash=sha256:df9b0f8f511270ad259c7bfba22ab6d5a0c33d81cd594461668e67cd80dd9052 \ + --hash=sha256:e043b79e39f165026bc941c95582bfc4bfdd297a1de6f13ace0d0a7abf486288 \ + --hash=sha256:e2686c37d22faf27d02a19e83b55812d248b32b7ba3aa638e768d0ea032e1f3c \ + --hash=sha256:e9a6251818b9eb6d519bffd7a0b745f3a99b3e99563a4c9d3cad26e34f6ac880 \ + --hash=sha256:eab6c253983a6659e749f4c44fcc2215194c2e00bf7b1c5e90fe683ea3b7b00f \ + --hash=sha256:ec64b7b3fb95bc9c20c72548277794b81281a6ba9da85eda2c87324c218441ff \ + --hash=sha256:ee62ec5882a857b252faffeb7867679f7e418052ca6bf7d6b56099f6498a2b0e \ + --hash=sha256:ee757fd36bad66ad8b961958840894021ecaad22194f65219a666432739393ff \ + --hash=sha256:f55623094b665d79a3b82ba77386ac34fa85049163edfe65387063e5127d4184 \ + --hash=sha256:f622f542bd065ffec7d26b26d44d0c9a25c9c1295fd8ba6e4d77778e2293a12c \ + --hash=sha256:f873af54014cac12082c7f5ccec6bbbeb5b57f63466e7f9c61a34588621313fb \ + --hash=sha256:fae24c875c4ecc8c5f34a9715eb2a459743b4ca21d35c51819b640ee2f71cb51 \ + --hash=sha256:fb26e69fc6c12534fbaa1657efed3b6482f1a166ba8e31227fa6f6f062a59070 + # via -r requirements-test.in +pytest==7.3.1 \ + --hash=sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362 \ + --hash=sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3 + +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f diff --git a/python_files/tests/test_data/no-missing-deps.data b/python_files/tests/test_data/no-missing-deps.data new file mode 100644 index 000000000000..d5d04476dec0 --- /dev/null +++ b/python_files/tests/test_data/no-missing-deps.data @@ -0,0 +1,13 @@ +# +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: +# +# pip-compile --generate-hashes --resolver=backtracking requirements-test.in +# +pytest==7.3.1 \ + --hash=sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362 \ + --hash=sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3 + +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f diff --git a/python_files/tests/test_data/pyproject-missing-deps.data b/python_files/tests/test_data/pyproject-missing-deps.data new file mode 100644 index 000000000000..e4d6f9eb10d3 --- /dev/null +++ b/python_files/tests/test_data/pyproject-missing-deps.data @@ -0,0 +1,9 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "something" +version = "2023.0.0" +requires-python = ">=3.8" +dependencies = ["pytest==7.3.1", "flake8-csv"] diff --git a/python_files/tests/test_data/pyproject-no-missing-deps.data b/python_files/tests/test_data/pyproject-no-missing-deps.data new file mode 100644 index 000000000000..64dadf6fdf2e --- /dev/null +++ b/python_files/tests/test_data/pyproject-no-missing-deps.data @@ -0,0 +1,9 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "something" +version = "2023.0.0" +requires-python = ">=3.8" +dependencies = [jedi-language-server"] diff --git a/python_files/tests/test_dynamic_cursor.py b/python_files/tests/test_dynamic_cursor.py new file mode 100644 index 000000000000..d30887c24d5b --- /dev/null +++ b/python_files/tests/test_dynamic_cursor.py @@ -0,0 +1,192 @@ +import importlib +import textwrap + +import normalizeSelection + + +def test_dictionary_mouse_mover(): + """Having the mouse cursor on second line, 'my_dict = {' and pressing shift+enter should bring the mouse cursor to line 6, on and to be able to run 'print('only send the dictionary')'.""" + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + not_dictionary = 'hi' + my_dict = { + "key1": "value1", + "key2": "value2" + } + print('only send the dictionary') + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, was_highlighted=False) + + assert result["which_line_next"] == 6 + + +def test_beginning_func(): + """Pressing shift+enter on the very first line, of function definition, such as 'my_func():'. + + It should properly skip the comment and assert the next executable line to be + executed is line 5 at 'my_dict = {'. + """ + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def my_func(): + print("line 2") + print("line 3") + # Skip line 4 because it is a comment + my_dict = { + "key1": "value1", + "key2": "value2" + } + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + + assert result["which_line_next"] == 5 + + +def test_cursor_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + lucid_dream = ["Corgi", "Husky", "Pomsky"] + for dogs in lucid_dream: # initial starting position + print(dogs) + print("I wish I had a dog!") + + print("This should be the next block that should be ran") + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, was_highlighted=False) + + assert result["which_line_next"] == 6 + + +def test_inside_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for food in lucid_dream: + print("We are starting") # initial starting position + print("Next cursor should be here!") + + """ + ) + + result = normalizeSelection.traverse_file(src, 2, 2, was_highlighted=False) + + assert result["which_line_next"] == 3 + + +def test_skip_sameline_statements(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("Audi");print("BMW");print("Mercedes") + print("Next line to be run is here!") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + + assert result["which_line_next"] == 2 + + +def test_skip_multi_comp_lambda(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + + my_lambda = lambda x: ( + x + 1 + ) + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + # Shift enter from the very first ( should make + # next executable statement as the lambda expression + assert result["which_line_next"] == 7 + + +def test_move_whole_class(): + """Shift+enter on a class definition should move the cursor after running whole class.""" + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + print("We should be here after running whole class") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + + assert result["which_line_next"] == 7 + + +def test_def_to_def(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + + # Skip here + def next_func(): + print("Not here but above") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + + assert result["which_line_next"] == 9 + + +def test_try_catch_move(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + print("Should be here afterwards") + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + assert result["which_line_next"] == 6 + + +def test_skip_nested(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + + print("Cursor should be here after running line 1") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + assert result["which_line_next"] == 8 diff --git a/python_files/tests/test_get_variable_info.py b/python_files/tests/test_get_variable_info.py new file mode 100644 index 000000000000..73f94fe26f06 --- /dev/null +++ b/python_files/tests/test_get_variable_info.py @@ -0,0 +1,114 @@ +import get_variable_info + + +def set_global_variable(value): + # setting on the module allows tests to set a variable that the module under test can access + get_variable_info.test_variable = value # pyright: ignore[reportGeneralTypeIssues] + + +def get_global_variable(): + results = get_variable_info.getVariableDescriptions() + for variable in results: + if variable["name"] == "test_variable": + return variable + return None + + +def assert_variable_found(variable, expected_value, expected_type, expected_count=None): + set_global_variable(variable) + variable = get_global_variable() + assert variable is not None + if expected_value is not None: + assert variable["value"] == expected_value + assert variable["type"] == expected_type + if expected_count is not None: + assert variable["count"] == expected_count + else: + assert "count" not in variable + return variable + + +def assert_indexed_child(variable, start_index, expected_index, expected_child_value=None): + children = get_variable_info.getAllChildrenDescriptions( + variable["root"], variable["propertyChain"], start_index + ) + child = children[expected_index] + + if expected_child_value is not None: + assert child["value"] == expected_child_value + return child + + +def assert_property(variable, expected_property_name, expected_property_value=None): + children = get_variable_info.getAllChildrenDescriptions( + variable["root"], variable["propertyChain"], 0 + ) + found = None + for child in children: + chain = child["propertyChain"] + property_name = chain[-1] if chain else None + if property_name == expected_property_name: + found = child + break + + assert found is not None + if expected_property_value is not None: + assert found["value"] == expected_property_value + return found + + +def test_simple(): + assert_variable_found(1, "1", "int", None) + + +def test_list(): + found = assert_variable_found([1, 2, 3], "[1, 2, 3]", "list", 3) + assert_indexed_child(found, 0, 0, "1") + + +def test_dict(): + found = assert_variable_found({"a": 1, "b": 2}, "{'a': 1, 'b': 2}", "dict", None) + assert found["hasNamedChildren"] + assert_property(found, "a", "1") + assert_property(found, "b", "2") + + +def test_tuple(): + found = assert_variable_found((1, 2, 3), "(1, 2, 3)", "tuple", 3) + assert_indexed_child(found, 0, 0, "1") + + +def test_set(): + found = assert_variable_found({1, 2, 3}, "{1, 2, 3}", "set", 3) + assert_indexed_child(found, 0, 0, "1") + + +def test_self_referencing_dict(): + d = {} + d["self"] = d + found = assert_variable_found(d, "{'self': {...}}", "dict", None) + assert_property(found, "self", "{'self': {...}}") + + +def test_nested_list(): + found = assert_variable_found([[1, 2], [3, 4]], "[[1, 2], [3, 4]]", "list", 2) + assert_indexed_child(found, 0, 0, "[1, 2]") + + +def test_long_list(): + child = assert_variable_found(list(range(1_000_000)), None, "list", 1_000_000) + value = child["value"] + assert value.startswith("[0, 1, 2, 3") + assert value.endswith("...]") + assert_indexed_child(child, 400_000, 10, "400010") + assert_indexed_child(child, 999_950, 10, "999960") + + +def test_get_nested_children(): + d = [{"a": {("hello")}}] + found = assert_variable_found(d, "[{'a': {...}}]", "list", 1) + + found = assert_indexed_child(found, 0, 0) + found = assert_property(found, "a") + found = assert_indexed_child(found, 0, 0) + assert found["value"] == "'hello'" diff --git a/python_files/tests/test_installed_check.py b/python_files/tests/test_installed_check.py new file mode 100644 index 000000000000..607e02f34abd --- /dev/null +++ b/python_files/tests/test_installed_check.py @@ -0,0 +1,138 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import contextlib +import json +import os +import pathlib +import subprocess +import sys +from typing import Dict, List, Optional, Union + +import pytest + +SCRIPT_PATH = pathlib.Path(__file__).parent.parent / "installed_check.py" +TEST_DATA = pathlib.Path(__file__).parent / "test_data" +DEFAULT_SEVERITY = 3 + + +@contextlib.contextmanager +def generate_file(base_file: pathlib.Path): + basename = "pyproject.toml" if "pyproject" in base_file.name else "requirements.txt" + fullpath = base_file.parent / basename + if fullpath.exists(): + fullpath.unlink() + fullpath.write_text(base_file.read_text(encoding="utf-8")) + try: + yield fullpath + finally: + fullpath.unlink() + + +def run_on_file( + file_path: pathlib.Path, severity: Optional[str] = None +) -> List[Dict[str, Union[str, int]]]: + env = os.environ.copy() + if severity: + env["VSCODE_MISSING_PGK_SEVERITY"] = severity + result = subprocess.run( + [ + sys.executable, + os.fspath(SCRIPT_PATH), + os.fspath(file_path), + ], + capture_output=True, + check=True, + env=env, + ) + assert result.returncode == 0 + assert result.stderr == b"" + return json.loads(result.stdout) + + +EXPECTED_DATA = { + "missing-deps": [ + { + "line": 6, + "character": 0, + "endLine": 6, + "endCharacter": 10, + "package": "flake8-csv", + "code": "not-installed", + "severity": 3, + }, + { + "line": 10, + "character": 0, + "endLine": 10, + "endCharacter": 11, + "package": "levenshtein", + "code": "not-installed", + "severity": 3, + }, + ], + "no-missing-deps": [], + "pyproject-missing-deps": [ + { + "line": 8, + "character": 34, + "endLine": 8, + "endCharacter": 44, + "package": "flake8-csv", + "code": "not-installed", + "severity": 3, + } + ], + "pyproject-no-missing-deps": [], +} + + +@pytest.mark.parametrize("test_name", EXPECTED_DATA.keys()) +def test_installed_check(test_name: str): + base_file = TEST_DATA / f"{test_name}.data" + with generate_file(base_file) as file_path: + result = run_on_file(file_path) + assert result == EXPECTED_DATA[test_name] + + +EXPECTED_DATA2 = { + "missing-deps": [ + { + "line": 6, + "character": 0, + "endLine": 6, + "endCharacter": 10, + "package": "flake8-csv", + "code": "not-installed", + "severity": 0, + }, + { + "line": 10, + "character": 0, + "endLine": 10, + "endCharacter": 11, + "package": "levenshtein", + "code": "not-installed", + "severity": 0, + }, + ], + "pyproject-missing-deps": [ + { + "line": 8, + "character": 34, + "endLine": 8, + "endCharacter": 44, + "package": "flake8-csv", + "code": "not-installed", + "severity": 0, + } + ], +} + + +@pytest.mark.parametrize("test_name", EXPECTED_DATA2.keys()) +def test_with_severity(test_name: str): + base_file = TEST_DATA / f"{test_name}.data" + with generate_file(base_file) as file_path: + result = run_on_file(file_path, severity="0") + assert result == EXPECTED_DATA2[test_name] diff --git a/python_files/tests/test_normalize_selection.py b/python_files/tests/test_normalize_selection.py new file mode 100644 index 000000000000..779bb9720bfa --- /dev/null +++ b/python_files/tests/test_normalize_selection.py @@ -0,0 +1,317 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +import importlib +import textwrap + +# __file__ = "/Users/anthonykim/Desktop/vscode-python/python_files/normalizeSelection.py" +# sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__)))) +import normalizeSelection + + +class TestNormalizationScript: + """Unit tests for the normalization script.""" + + def test_basic_normalization(self): + src = 'print("this is a test")' + expected = src + "\n" + result = normalizeSelection.normalize_lines(src) + assert result == expected + + def test_more_than_one_line(self): + src = textwrap.dedent( + """\ + # Some rando comment + + def show_something(): + print("Something") + """ + ) + expected = textwrap.dedent( + """\ + def show_something(): + print("Something") + + """ + ) + result = normalizeSelection.normalize_lines(src) + assert result == expected + + def test_with_hanging_indent(self): + 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") + """ + ) + expected = textwrap.dedent( + """\ + x = 22 + y = 30 + z = -10 + result = x + y + z + if result == 42: + print("The answer to life, the universe, and everything") + + """ + ) + result = normalizeSelection.normalize_lines(src) + assert result == expected + + def test_clear_out_extraneous_newlines(self): + src = textwrap.dedent( + """\ + value_x = 22 + + value_y = 30 + + value_z = -10 + + print(value_x + value_y + value_z) + + """ + ) + expected = textwrap.dedent( + """\ + value_x = 22 + value_y = 30 + value_z = -10 + print(value_x + value_y + value_z) + """ + ) + result = normalizeSelection.normalize_lines(src) + assert result == expected + + def test_clear_out_extra_lines_and_whitespace(self): + src = textwrap.dedent( + """\ + if True: + x = 22 + + y = 30 + + z = -10 + + print(x + y + z) + + """ + ) + expected = textwrap.dedent( + """\ + if True: + x = 22 + y = 30 + z = -10 + + print(x + y + z) + """ + ) + result = normalizeSelection.normalize_lines(src) + assert result == expected + + def test_partial_single_line(self): + src = " print('foo')" + expected = textwrap.dedent(src) + "\n" + result = normalizeSelection.normalize_lines(src) + assert result == expected + + def test_multiline_with_indent(self): + src = """\ + + if (x > 0 + and condition == True): + print('foo') + else: + + print('bar') + """ + + expected = textwrap.dedent( + """\ + if (x > 0 + and condition == True): + print('foo') + else: + print('bar') + + """ + ) + + result = normalizeSelection.normalize_lines(src) + assert result == expected + + def test_multiline_with_comment(self): + src = textwrap.dedent( + """\ + + def show_something(): + # A comment + print("Something") + """ + ) + expected = textwrap.dedent( + """\ + def show_something(): + # A comment + print("Something") + + """ + ) + result = normalizeSelection.normalize_lines(src) + assert result == expected + + def test_exception(self): + src = " if True:" + expected = src + "\n\n" + result = normalizeSelection.normalize_lines(src) + assert result == expected + + def test_multiline_exception(self): + src = textwrap.dedent( + """\ + + def show_something(): + if True: + """ + ) + expected = src + "\n\n" + result = normalizeSelection.normalize_lines(src) + assert result == expected + + def test_decorators(self): + src = textwrap.dedent( + """\ + def foo(func): + + def wrapper(): + print('before') + func() + print('after') + + return wrapper + + + @foo + def show_something(): + print("Something") + """ + ) + expected = textwrap.dedent( + """\ + def foo(func): + def wrapper(): + print('before') + func() + print('after') + return wrapper + + @foo + def show_something(): + print("Something") + + """ + ) + result = normalizeSelection.normalize_lines(src) + assert result == expected + + def test_fstring(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + name = "Ahri" + age = 10 + + print(f'My name is {name}') + """ + ) + + expected = textwrap.dedent( + """\ + name = "Ahri" + age = 10 + print(f'My name is {name}') + """ + ) + result = normalizeSelection.normalize_lines(src) + + assert result == expected + + def test_list_comp(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + names = ['Ahri', 'Bobby', 'Charlie'] + breed = ['Pomeranian', 'Welsh Corgi', 'Siberian Husky'] + dogs = [(name, breed) for name, breed in zip(names, breed)] + + print(dogs) + my_family_dog = 'Corgi' + """ + ) + + expected = textwrap.dedent( + """\ + names = ['Ahri', 'Bobby', 'Charlie'] + breed = ['Pomeranian', 'Welsh Corgi', 'Siberian Husky'] + dogs = [(name, breed) for name, breed in zip(names, breed)] + print(dogs) + my_family_dog = 'Corgi' + """ + ) + + result = normalizeSelection.normalize_lines(src) + + assert result == expected + + def test_return_dict(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def get_dog(name, breed): + return {'name': name, 'breed': breed} + """ + ) + + expected = textwrap.dedent( + """\ + def get_dog(name, breed): + return {'name': name, 'breed': breed} + + """ + ) + + result = normalizeSelection.normalize_lines(src) + + assert result == expected + + def test_return_dict2(self): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + def get_dog(name, breed): + return {'name': name, 'breed': breed} + + dog = get_dog('Ahri', 'Pomeranian') + print(dog) + """ + ) + + expected = textwrap.dedent( + """\ + def get_dog(name, breed): + return {'name': name, 'breed': breed} + + dog = get_dog('Ahri', 'Pomeranian') + print(dog) + """ + ) + + result = normalizeSelection.normalize_lines(src) + + assert result == expected diff --git a/python_files/tests/test_python_server.py b/python_files/tests/test_python_server.py new file mode 100644 index 000000000000..ca542b8ea292 --- /dev/null +++ b/python_files/tests/test_python_server.py @@ -0,0 +1,162 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +"""Tests for python_server.py, specifically EOF handling to prevent infinite loops.""" + +import io +from unittest import mock + +import pytest + + +class TestGetHeaders: + """Tests for the get_headers function.""" + + def test_get_headers_normal(self): + """Test get_headers with valid headers.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin with valid headers + mock_input = b"Content-Length: 100\r\nContent-Type: application/json\r\n\r\n" + mock_stdin = io.BytesIO(mock_input) + + # Act + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)): + headers = python_server.get_headers() + + # Assert + assert headers == {"Content-Length": "100", "Content-Type": "application/json"} + + def test_get_headers_eof_raises_error(self): + """Test that get_headers raises EOFError when stdin is closed (EOF).""" + # Arrange: Import the module + import python_server + + # Create a mock stdin that returns empty bytes (EOF) + mock_stdin = io.BytesIO(b"") + + # Act & Assert + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)), pytest.raises( + EOFError, match="EOF reached while reading headers" + ): + python_server.get_headers() + + def test_get_headers_eof_mid_headers_raises_error(self): + """Test that get_headers raises EOFError when EOF occurs mid-headers.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin with partial headers then EOF + mock_input = b"Content-Length: 100\r\n" # No terminating empty line + mock_stdin = io.BytesIO(mock_input) + + # Act & Assert + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)), pytest.raises( + EOFError, match="EOF reached while reading headers" + ): + python_server.get_headers() + + def test_get_headers_empty_line_terminates(self): + """Test that an empty line (not EOF) properly terminates header reading.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin with headers followed by empty line + mock_input = b"Content-Length: 50\r\n\r\nsome body content" + mock_stdin = io.BytesIO(mock_input) + + # Act + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)): + headers = python_server.get_headers() + + # Assert + assert headers == {"Content-Length": "50"} + + +class TestEOFHandling: + """Tests for EOF handling in various functions that use get_headers.""" + + def test_custom_input_exits_on_eof(self): + """Test that custom_input exits gracefully on EOF.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin that returns empty bytes (EOF) + mock_stdin = io.BytesIO(b"") + mock_stdout = io.BytesIO() + + # Act & Assert + with mock.patch.object( + python_server, "STDIN", mock.Mock(buffer=mock_stdin) + ), mock.patch.object(python_server, "STDOUT", mock.Mock(buffer=mock_stdout)), pytest.raises( + SystemExit + ) as exc_info: + python_server.custom_input("prompt> ") + + # Should exit with code 0 (graceful exit) + assert exc_info.value.code == 0 + + def test_handle_response_exits_on_eof(self): + """Test that handle_response exits gracefully on EOF.""" + # Arrange: Import the module + import python_server + + # Create a mock stdin that returns empty bytes (EOF) + mock_stdin = io.BytesIO(b"") + + # Act & Assert + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)), pytest.raises( + SystemExit + ) as exc_info: + python_server.handle_response("test-request-id") + + # Should exit with code 0 (graceful exit) + assert exc_info.value.code == 0 + + +class TestMainLoopEOFHandling: + """Tests that simulate the main loop EOF scenario.""" + + def test_main_loop_exits_on_eof(self): + """Test that the main loop pattern exits gracefully on EOF. + + This test verifies the fix for GitHub issue #25620 where the server + would spin at 100% CPU instead of exiting when VS Code closes. + """ + # Arrange: Import the module + import python_server + + # Create a mock stdin that returns empty bytes (EOF) + mock_stdin = io.BytesIO(b"") + + # Simulate what happens in the main loop + with mock.patch.object(python_server, "STDIN", mock.Mock(buffer=mock_stdin)): + try: + python_server.get_headers() + # If we get here without raising EOFError, the fix isn't working + pytest.fail("Expected EOFError to be raised on EOF") + except EOFError: + # This is the expected behavior - the fix is working + pass + + def test_readline_eof_vs_empty_line(self): + """Test that we correctly distinguish between EOF and empty line. + + EOF: readline() returns b'' (empty bytes) + Empty line: readline() returns b'\\r\\n' or b'\\n' (newline bytes) + """ + # Test EOF case + eof_stream = io.BytesIO(b"") + result = eof_stream.readline() + assert result == b"", "EOF should return empty bytes" + + # Test empty line case + empty_line_stream = io.BytesIO(b"\r\n") + result = empty_line_stream.readline() + assert result == b"\r\n", "Empty line should return newline bytes" + + # Test empty line with just newline + empty_line_stream2 = io.BytesIO(b"\n") + result = empty_line_stream2.readline() + assert result == b"\n", "Empty line should return newline bytes" diff --git a/python_files/tests/test_shell_integration.py b/python_files/tests/test_shell_integration.py new file mode 100644 index 000000000000..7503a725b6d1 --- /dev/null +++ b/python_files/tests/test_shell_integration.py @@ -0,0 +1,83 @@ +import importlib +import platform +import sys +from unittest.mock import Mock + +import pythonrc + +is_wsl = "microsoft-standard-WSL" in platform.release() + + +def test_decoration_success(): + importlib.reload(pythonrc) + ps1 = pythonrc.PS1() + + ps1.hooks.failure_flag = False + result = str(ps1) + if sys.platform != "win32" and (not is_wsl): + assert ( + result + == "\x01\x1b]633;C\x07\x1b]633;E;None\x07\x1b]633;D;0\x07\x1b]633;A\x07\x02>>> \x01\x1b]633;B\x07\x02" + ) + else: + pass + + +def test_decoration_failure(): + importlib.reload(pythonrc) + ps1 = pythonrc.PS1() + + ps1.hooks.failure_flag = True + result = str(ps1) + if sys.platform != "win32" and (not is_wsl): + assert ( + result + == "\x01\x1b]633;C\x07\x1b]633;E;None\x07\x1b]633;D;1\x07\x1b]633;A\x07\x02>>> \x01\x1b]633;B\x07\x02" + ) + else: + pass + + +def test_displayhook_call(): + importlib.reload(pythonrc) + pythonrc.PS1() + mock_displayhook = Mock() + + hooks = pythonrc.REPLHooks() + hooks.original_displayhook = mock_displayhook + + hooks.my_displayhook("mock_value") + + mock_displayhook.assert_called_once_with("mock_value") + + +def test_excepthook_call(): + importlib.reload(pythonrc) + pythonrc.PS1() + mock_excepthook = Mock() + + hooks = pythonrc.REPLHooks() + hooks.original_excepthook = mock_excepthook + + hooks.my_excepthook("mock_type", "mock_value", "mock_traceback") + mock_excepthook.assert_called_once_with("mock_type", "mock_value", "mock_traceback") + + +if sys.platform == "darwin": + + def test_print_statement_darwin(monkeypatch): + importlib.reload(pythonrc) + with monkeypatch.context() as m: + m.setattr("builtins.print", Mock()) + importlib.reload(sys.modules["pythonrc"]) + print.assert_any_call("Cmd click to launch VS Code Native REPL") + + +if sys.platform == "win32": + + def test_print_statement_non_darwin(monkeypatch): + importlib.reload(pythonrc) + with monkeypatch.context() as m: + m.setattr("builtins.print", Mock()) + importlib.reload(sys.modules["pythonrc"]) + print.assert_any_call("Ctrl click to launch VS Code Native REPL") diff --git a/python_files/tests/test_smart_selection.py b/python_files/tests/test_smart_selection.py new file mode 100644 index 000000000000..15b1b1a3ec02 --- /dev/null +++ b/python_files/tests/test_smart_selection.py @@ -0,0 +1,360 @@ +import importlib +import textwrap + +import normalizeSelection + + +def test_part_dictionary(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + not_dictionary = 'hi' + my_dict = { + "key1": "value1", + "key2": "value2" + } + print('only send the dictionary') + """ + ) + + expected = textwrap.dedent( + """\ + my_dict = { + "key1": "value1", + "key2": "value2" + } + """ + ) + + result = normalizeSelection.traverse_file(src, 3, 3, was_highlighted=False) + assert result["normalized_smart_result"] == expected + + +def test_nested_loop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + """ + ) + expected = textwrap.dedent( + """\ + for i in range(1, 6): + for j in range(1, 6): + for x in range(1, 5): + for y in range(1, 5): + for z in range(1,10): + print(i, j, x, y, z) + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + assert result["normalized_smart_result"] == expected + + +def test_smart_shift_enter_multiple_statements(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + import textwrap + import ast + + print("Porsche") + print("Genesis") + + + print("Audi");print("BMW");print("Mercedes") + + print("dont print me") + + """ + ) + # Expected to printing statement line by line, + # for when multiple print statements are ran + # from the same line. + expected = textwrap.dedent( + """\ + print("Audi") + print("BMW") + print("Mercedes") + """ + ) + result = normalizeSelection.traverse_file(src, 8, 8, was_highlighted=False) + assert result["normalized_smart_result"] == expected + + +def test_two_layer_dictionary(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("dont print me") + + two_layered_dictionary = { + 'inner_dict_one': { + 'Audi': 'Germany', + 'BMW': 'Germnay', + 'Genesis': 'Korea', + }, + 'inner_dict_two': { + 'Mercedes': 'Germany', + 'Porsche': 'Germany', + 'Lamborghini': 'Italy', + 'Ferrari': 'Italy', + 'Maserati': 'Italy' + } + } + """ + ) + expected = textwrap.dedent( + """\ + two_layered_dictionary = { + 'inner_dict_one': { + 'Audi': 'Germany', + 'BMW': 'Germnay', + 'Genesis': 'Korea', + }, + 'inner_dict_two': { + 'Mercedes': 'Germany', + 'Porsche': 'Germany', + 'Lamborghini': 'Italy', + 'Ferrari': 'Italy', + 'Maserati': 'Italy' + } + } + """ + ) + result = normalizeSelection.traverse_file(src, 6, 7, was_highlighted=False) + + assert result["normalized_smart_result"] == expected + + +def test_run_whole_func(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + print("Decide which dog you will choose") + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + """ + ) + + expected = textwrap.dedent( + """\ + def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + + """ + ) + result = normalizeSelection.traverse_file(src, 2, 2, was_highlighted=False) + + assert result["normalized_smart_result"] == expected + + +def test_small_forloop(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + """ + ) + expected = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + + """ + ) + + # Cover the whole for loop block with multiple inner statements + # Make sure to contain all of the print statements included. + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + + assert result["normalized_smart_result"] == expected + + +def inner_for_loop_component(): + """Pressing shift+enter inside a for loop, specifically on a viable expression by itself, such as print(i) should only return that exact expression.""" + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + for i in range(1, 6): + print(i) + print("Please also send this print statement") + """ + ) + result = normalizeSelection.traverse_file(src, 2, 2, was_highlighted=False) + expected = textwrap.dedent( + """\ + print(i) + """ + ) + + assert result["normalized_smart_result"] == expected + + +def test_dict_comprehension(): + """Having the mouse cursor on the first line, and pressing shift+enter should return the whole dictionary comp, respecting user's code style.""" + src = textwrap.dedent( + """\ + my_dict_comp = {temp_mover: + temp_mover for temp_mover in range(1, 7)} + """ + ) + + expected = textwrap.dedent( + """\ + my_dict_comp = {temp_mover: + temp_mover for temp_mover in range(1, 7)} + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + + assert result["normalized_smart_result"] == expected + + +def test_send_whole_generator(): + """Pressing shift+enter on the first line, which is the '(' should be returning the whole generator expression instead of just the '('.""" + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + """ + ) + + expected = textwrap.dedent( + """\ + ( + my_first_var + for my_first_var in range(1, 10) + if my_first_var % 2 == 0 + ) + + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + + assert result["normalized_smart_result"] == expected + + +def test_multiline_lambda(): + """Shift+enter on part of the lambda expression should return the whole lambda expression, regardless of whether all the component of lambda expression is on the same or not.""" + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + my_lambda = lambda x: ( + x + 1 + ) + """ + ) + expected = textwrap.dedent( + """\ + my_lambda = lambda x: ( + x + 1 + ) + + """ + ) + + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + assert result["normalized_smart_result"] == expected + + +def test_send_whole_class(): + """Shift+enter on a class definition should send the whole class definition.""" + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + print("We should be here after running whole class") + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + expected = textwrap.dedent( + """\ + class Stub(object): + def __init__(self): + self.calls = [] + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + + """ + ) + assert result["normalized_smart_result"] == expected + + +def test_send_whole_if_statement(): + """Shift+enter on an if statement should send the whole if statement including statements inside and else.""" + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + if True: + print('send this') + else: + print('also send this') + + print('cursor here afterwards') + """ + ) + expected = textwrap.dedent( + """\ + if True: + print('send this') + else: + print('also send this') + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + assert result["normalized_smart_result"] == expected + + +def test_send_try(): + importlib.reload(normalizeSelection) + src = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + print("Not running this") + """ + ) + expected = textwrap.dedent( + """\ + try: + 1+1 + except: + print("error") + + """ + ) + result = normalizeSelection.traverse_file(src, 1, 1, was_highlighted=False) + assert result["normalized_smart_result"] == expected diff --git a/python_files/tests/tree_comparison_helper.py b/python_files/tests/tree_comparison_helper.py new file mode 100644 index 000000000000..3d9d1d39194b --- /dev/null +++ b/python_files/tests/tree_comparison_helper.py @@ -0,0 +1,39 @@ +def is_same_tree(tree1, tree2, test_key_arr, path="root") -> bool: + """Helper function to test if two test trees are the same with detailed error logs. + + `is_same_tree` starts by comparing the root attributes, and then checks if all children are the same. + """ + # Compare the root. + for key in ["path", "name", "type_", "id_"]: + if tree1.get(key) != tree2.get(key): + print( + f"Difference found at {path}: '{key}' is '{tree1.get(key)}' in tree1 and '{tree2.get(key)}' in tree2." + ) + return False + + # Compare child test nodes if they exist, otherwise compare test items. + if "children" in tree1 and "children" in tree2: + # Sort children by path before comparing since order doesn't matter of children + children1 = sorted(tree1["children"], key=lambda x: x["path"]) + children2 = sorted(tree2["children"], key=lambda x: x["path"]) + + # Compare test nodes. + if len(children1) != len(children2): + print( + f"Difference in number of children at {path}: {len(children1)} in tree1 and {len(children2)} in tree2." + ) + return False + else: + for i, (child1, child2) in enumerate(zip(children1, children2)): + if not is_same_tree(child1, child2, test_key_arr, path=f"{path} -> child {i}"): + return False + elif "id_" in tree1 and "id_" in tree2: + # Compare test items. + for key in test_key_arr: + if tree1.get(key) != tree2.get(key): + print( + f"Difference found at {path}: '{key}' is '{tree1.get(key)}' in tree1 and '{tree2.get(key)}' in tree2." + ) + return False + + return True diff --git a/python_files/tests/unittestadapter/.data/coverage_ex/__init__.py b/python_files/tests/unittestadapter/.data/coverage_ex/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/coverage_ex/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/.data/coverage_ex/reverse.py b/python_files/tests/unittestadapter/.data/coverage_ex/reverse.py new file mode 100644 index 000000000000..4840b7d05bf3 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/coverage_ex/reverse.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +def reverse_string(s): + if s is None or s == "": + return "Error: Input is None" + return s[::-1] + +def reverse_sentence(sentence): + if sentence is None or sentence == "": + return "Error: Input is None" + words = sentence.split() + reversed_words = [reverse_string(word) for word in words] + return " ".join(reversed_words) diff --git a/python_files/tests/unittestadapter/.data/coverage_ex/test_reverse.py b/python_files/tests/unittestadapter/.data/coverage_ex/test_reverse.py new file mode 100644 index 000000000000..2521e3dc1935 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/coverage_ex/test_reverse.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +from reverse import reverse_sentence, reverse_string + +class TestReverseFunctions(unittest.TestCase): + + def test_reverse_sentence(self): + """ + Tests the reverse_sentence function to ensure it correctly reverses each word in a sentence. + + Test cases: + - "hello world" should be reversed to "olleh dlrow" + - "Python is fun" should be reversed to "nohtyP si nuf" + - "a b c" should remain "a b c" as each character is a single word + """ + self.assertEqual(reverse_sentence("hello world"), "olleh dlrow") + self.assertEqual(reverse_sentence("Python is fun"), "nohtyP si nuf") + self.assertEqual(reverse_sentence("a b c"), "a b c") + + def test_reverse_sentence_error(self): + self.assertEqual(reverse_sentence(""), "Error: Input is None") + self.assertEqual(reverse_sentence(None), "Error: Input is None") + + def test_reverse_string(self): + self.assertEqual(reverse_string("hello"), "olleh") + self.assertEqual(reverse_string("Python"), "nohtyP") + # this test specifically does not cover the error cases + +if __name__ == '__main__': + unittest.main() diff --git a/python_files/tests/unittestadapter/.data/discovery_empty.py b/python_files/tests/unittestadapter/.data/discovery_empty.py new file mode 100644 index 000000000000..9af5071303ce --- /dev/null +++ b/python_files/tests/unittestadapter/.data/discovery_empty.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + + +class DiscoveryEmpty(unittest.TestCase): + """Test class for the test_empty_discovery test. + + The discover_tests function should return a dictionary with a "success" status, no errors, and no test tree + if unittest discovery was performed successfully but no tests were found. + """ + + def something(self) -> bool: + return True diff --git a/python_files/tests/unittestadapter/.data/discovery_error/file_one.py b/python_files/tests/unittestadapter/.data/discovery_error/file_one.py new file mode 100644 index 000000000000..031b6f6c9d68 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/discovery_error/file_one.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + +import something_else # type: ignore # noqa: F401 + + +class DiscoveryErrorOne(unittest.TestCase): + """Test class for the test_error_discovery test. + + The discover_tests function should return a dictionary with an "error" status, the discovered tests, and a list of errors + if unittest discovery failed at some point. + """ + + def test_one(self) -> None: + self.assertGreater(2, 1) + + def test_two(self) -> None: + self.assertNotEqual(2, 1) diff --git a/python_files/tests/unittestadapter/.data/discovery_error/file_two.py b/python_files/tests/unittestadapter/.data/discovery_error/file_two.py new file mode 100644 index 000000000000..5d6d54f886a1 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/discovery_error/file_two.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + + +class DiscoveryErrorTwo(unittest.TestCase): + """Test class for the test_error_discovery test. + + The discover_tests function should return a dictionary with an "error" status, the discovered tests, and a list of errors + if unittest discovery failed at some point. + """ + + def test_one(self) -> None: + self.assertGreater(2, 1) + + def test_two(self) -> None: + self.assertNotEqual(2, 1) diff --git a/python_files/tests/unittestadapter/.data/discovery_simple.py b/python_files/tests/unittestadapter/.data/discovery_simple.py new file mode 100644 index 000000000000..1859436d5b5b --- /dev/null +++ b/python_files/tests/unittestadapter/.data/discovery_simple.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + + +class DiscoverySimple(unittest.TestCase): + """Test class for the test_simple_discovery test. + + The discover_tests function should return a dictionary with a "success" status, no errors, and a test tree + if unittest discovery was performed successfully. + """ + + def test_one(self) -> None: + self.assertGreater(2, 1) + + def test_two(self) -> None: + self.assertNotEqual(2, 1) diff --git a/python_files/tests/unittestadapter/.data/doctest_patched_module.py b/python_files/tests/unittestadapter/.data/doctest_patched_module.py new file mode 100644 index 000000000000..636c5320b6d6 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/doctest_patched_module.py @@ -0,0 +1,17 @@ +""" +Patched doctest module. +This module's doctests will be patched to have proper IDs. + +>>> 2 + 2 +4 +""" + + +def example_function(): + """ + Example function with doctest. + + >>> example_function() + 'works' + """ + return "works" diff --git a/python_files/tests/unittestadapter/.data/doctest_standard.py b/python_files/tests/unittestadapter/.data/doctest_standard.py new file mode 100644 index 000000000000..52a10aa46a7f --- /dev/null +++ b/python_files/tests/unittestadapter/.data/doctest_standard.py @@ -0,0 +1,7 @@ +""" +Standard doctest module that should be blocked. +This has a simple doctest with short ID. + +>>> 2 + 2 +4 +""" diff --git a/python_files/tests/unittestadapter/.data/simple_django/db.sqlite3 b/python_files/tests/unittestadapter/.data/simple_django/db.sqlite3 new file mode 100644 index 000000000000..519ec5e1a11c Binary files /dev/null and b/python_files/tests/unittestadapter/.data/simple_django/db.sqlite3 differ diff --git a/python_files/tests/unittestadapter/.data/simple_django/manage.py b/python_files/tests/unittestadapter/.data/simple_django/manage.py new file mode 100755 index 000000000000..c5734a6babee --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/manage.py @@ -0,0 +1,23 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/python_files/tests/unittestadapter/.data/simple_django/mysite/__init__.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/mysite/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/.data/simple_django/mysite/asgi.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/asgi.py new file mode 100644 index 000000000000..bb01f607934c --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/mysite/asgi.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + +application = get_asgi_application() diff --git a/python_files/tests/unittestadapter/.data/simple_django/mysite/settings.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/settings.py new file mode 100644 index 000000000000..3120fb4e829f --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/mysite/settings.py @@ -0,0 +1,102 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +""" +Django settings for mysite project. + +Generated by 'django-admin startproject' using Django 3.2.22. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "polls.apps.PollsConfig", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'mysite.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'mysite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/python_files/tests/unittestadapter/.data/simple_django/mysite/urls.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/urls.py new file mode 100644 index 000000000000..02e76f125c72 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/mysite/urls.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path("polls/", include("polls.urls")), + path("admin/", admin.site.urls), +] diff --git a/python_files/tests/unittestadapter/.data/simple_django/mysite/wsgi.py b/python_files/tests/unittestadapter/.data/simple_django/mysite/wsgi.py new file mode 100644 index 000000000000..e932bff6649e --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/mysite/wsgi.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os + +from django.core.wsgi import get_wsgi_application + +application = get_wsgi_application() diff --git a/python_files/tests/unittestadapter/.data/simple_django/old_manage.py b/python_files/tests/unittestadapter/.data/simple_django/old_manage.py new file mode 100755 index 000000000000..844b98b4edba --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/old_manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +import os +import sys +if __name__ == "__main__": + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/__init__.py b/python_files/tests/unittestadapter/.data/simple_django/polls/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/admin.py b/python_files/tests/unittestadapter/.data/simple_django/polls/admin.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/admin.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/apps.py b/python_files/tests/unittestadapter/.data/simple_django/polls/apps.py new file mode 100644 index 000000000000..e31968ce16c0 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/apps.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from django.apps import AppConfig +from django.utils.functional import cached_property + + +class PollsConfig(AppConfig): + @cached_property + def default_auto_field(self): + return "django.db.models.BigAutoField" + + name = "polls" diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/0001_initial.py b/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/0001_initial.py new file mode 100644 index 000000000000..e33d24a3f704 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/0001_initial.py @@ -0,0 +1,52 @@ +# Generated by Django 5.0.8 on 2024-08-09 20:04 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Question", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("question_text", models.CharField(max_length=200, default="")), + ("pub_date", models.DateTimeField(verbose_name="date published", auto_now_add=True)), + ], + ), + migrations.CreateModel( + name="Choice", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("choice_text", models.CharField(max_length=200)), + ("votes", models.IntegerField(default=0)), + ( + "question", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="polls.question" + ), + ), + ], + ), + ] diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/__init__.py b/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/migrations/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/models.py b/python_files/tests/unittestadapter/.data/simple_django/polls/models.py new file mode 100644 index 000000000000..260a3da60f99 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/models.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from django.db import models +from django.utils import timezone +import datetime + + +class Question(models.Model): + question_text = models.CharField(max_length=200) + pub_date = models.DateTimeField("date published") + def __str__(self): + return self.question_text + def was_published_recently(self): + if self.pub_date > timezone.now(): + return False + return self.pub_date >= timezone.now() - datetime.timedelta(days=1) + + +class Choice(models.Model): + question = models.ForeignKey(Question, on_delete=models.CASCADE) + choice_text = models.CharField(max_length=200) + votes = models.IntegerField() + def __str__(self): + return self.choice_text diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/tests.py b/python_files/tests/unittestadapter/.data/simple_django/polls/tests.py new file mode 100644 index 000000000000..243262f195a8 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/tests.py @@ -0,0 +1,38 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from django.utils import timezone +from django.test import TestCase +from .models import Question +import datetime + +class QuestionModelTests(TestCase): + def test_was_published_recently_with_future_question(self): + """ + was_published_recently() returns False for questions whose pub_date + is in the future. + """ + time = timezone.now() + datetime.timedelta(days=30) + future_question: Question = Question.objects.create(pub_date=time) + self.assertIs(future_question.was_published_recently(), False) + + def test_was_published_recently_with_future_question_2(self): + """ + was_published_recently() returns False for questions whose pub_date + is in the future. + """ + time = timezone.now() + datetime.timedelta(days=30) + future_question = Question.objects.create(pub_date=time) + self.assertIs(future_question.was_published_recently(), True) + + def test_question_creation_and_retrieval(self): + """ + Test that a Question can be created and retrieved from the database. + """ + time = timezone.now() + question = Question.objects.create(pub_date=time, question_text="What's new?") + retrieved_question = Question.objects.get(question_text=question.question_text) + self.assertEqual(question, retrieved_question) + self.assertEqual(retrieved_question.question_text, "What's new?") + self.assertEqual(retrieved_question.pub_date, time) + diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/urls.py b/python_files/tests/unittestadapter/.data/simple_django/polls/urls.py new file mode 100644 index 000000000000..5756c7daa847 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/urls.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from django.urls import path + +from . import views + +urlpatterns = [ + # ex: /polls/ + path("", views.index, name="index"), +] diff --git a/python_files/tests/unittestadapter/.data/simple_django/polls/views.py b/python_files/tests/unittestadapter/.data/simple_django/polls/views.py new file mode 100644 index 000000000000..cccb6b3b0685 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/simple_django/polls/views.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from django.http import HttpResponse +from .models import Question # noqa: F401 + +def index(request): + return HttpResponse("Hello, world. You're at the polls index.") diff --git a/python_files/tests/unittestadapter/.data/test_doctest_patched.py b/python_files/tests/unittestadapter/.data/test_doctest_patched.py new file mode 100644 index 000000000000..3a719c7139ca --- /dev/null +++ b/python_files/tests/unittestadapter/.data/test_doctest_patched.py @@ -0,0 +1,50 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Test file with patched doctest integration that should work.""" + +import unittest +import doctest +import sys +import doctest_patched_module + + +# Patch DocTestCase to modify test IDs to be compatible with the extension +original_init = doctest.DocTestCase.__init__ + + +def patched_init(self, test, optionflags=0, setUp=None, tearDown=None, checker=None): + """Patch to modify doctest names to have proper hierarchy.""" + if hasattr(test, 'name'): + # Get module name + module_hierarchy = test.name.split('.') + module_name = module_hierarchy[0] if module_hierarchy else 'unknown' + + # Reconstruct with proper formatting to have enough components + # Format: module.file.class.function + if test.filename.endswith('.py'): + file_base = test.filename.split('/')[-1].replace('.py', '') + test_name = test.name.split('.')[-1] if '.' in test.name else test.name + # Create a properly formatted ID with enough components + test.name = f"{module_name}.{file_base}._DocTests.{test_name}" + + # Call original init + original_init(self, test, optionflags, setUp, tearDown, checker) + + +# Apply the patch +doctest.DocTestCase.__init__ = patched_init + + +def load_tests(loader, tests, ignore): + """ + Standard hook for unittest to load tests. + This uses patched doctest to create compatible test IDs. + """ + tests.addTests(doctest.DocTestSuite(doctest_patched_module)) + return tests + + +# Clean up the patch after loading +def tearDownModule(): + """Restore original DocTestCase.__init__""" + doctest.DocTestCase.__init__ = original_init diff --git a/python_files/tests/unittestadapter/.data/test_doctest_standard.py b/python_files/tests/unittestadapter/.data/test_doctest_standard.py new file mode 100644 index 000000000000..f5dba1209b98 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/test_doctest_standard.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Test file with standard doctest integration that should be blocked.""" + +import unittest +import doctest +import doctest_standard + + +def load_tests(loader, tests, ignore): + """ + Standard hook for unittest to load tests. + This uses standard doctest without any patching. + """ + tests.addTests(doctest.DocTestSuite(doctest_standard)) + return tests diff --git a/python_files/tests/unittestadapter/.data/test_fail_simple.py b/python_files/tests/unittestadapter/.data/test_fail_simple.py new file mode 100644 index 000000000000..e329c3fd7003 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/test_fail_simple.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + +# Test class for the test_fail_simple test. +# The test_failed_tests function should return a dictionary with a "success" status +# and the two tests with their outcome as "failed". + +class RunFailSimple(unittest.TestCase): + """Test class for the test_fail_simple test. + + The test_failed_tests function should return a dictionary with a "success" status + and the two tests with their outcome as "failed". + """ + + def test_one_fail(self) -> None: + self.assertGreater(2, 3) + + def test_two_fail(self) -> None: + self.assertNotEqual(1, 1) diff --git a/python_files/tests/unittestadapter/.data/test_scenarios/tests/__init__.py b/python_files/tests/unittestadapter/.data/test_scenarios/tests/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/test_scenarios/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/.data/test_scenarios/tests/test_scene.py b/python_files/tests/unittestadapter/.data/test_scenarios/tests/test_scene.py new file mode 100644 index 000000000000..35c1c7002319 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/test_scenarios/tests/test_scene.py @@ -0,0 +1,39 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + +from testscenarios import TestWithScenarios, generate_scenarios + + +def load_tests(loader, standard_tests, pattern): # noqa: ARG001 + # Pre-expand ``TestWithScenarios`` scenarios at load time so individual + # scenario-multiplied test IDs (e.g. ``test_operations(add)``) can be + # resolved by ``unittest.TestLoader.loadTestsFromName``. Without this, + # ``TestWithScenarios`` only multiplies scenarios at ``run()`` time and + # loading a specific scenario by name raises ``AttributeError``. + result = unittest.TestSuite() + result.addTests(generate_scenarios(standard_tests)) + return result + + +class TestMathOperations(TestWithScenarios): + scenarios = [ + ('add', {'test_id': 'test_add', 'a': 5, 'b': 3, 'expected': 8}), + ('subtract', {'test_id': 'test_subtract', 'a': 5, 'b': 3, 'expected': 2}), + ('multiply', {'test_id': 'test_multiply', 'a': 5, 'b': 3, 'expected': 15}), + ] + a: int = 0 + b: int = 0 + expected: int = 0 + test_id: str = "" + + def test_operations(self): + result = None + if self.test_id == 'test_add': + result = self.a + self.b + elif self.test_id == 'test_subtract': + result = self.a - self.b + elif self.test_id == 'test_multiply': + result = self.a * self.b + self.assertEqual(result, self.expected) diff --git a/python_files/tests/unittestadapter/.data/test_subtest.py b/python_files/tests/unittestadapter/.data/test_subtest.py new file mode 100644 index 000000000000..b913b8773701 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/test_subtest.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + +# Test class for the test_subtest_run test. +# The test_failed_tests function should return a dictionary that has a "success" status +# and the "result" value is a dict with 6 entries, one for each subtest. + + +class NumbersTest(unittest.TestCase): + def test_even(self): + """ + Test that numbers between 0 and 5 are all even. + """ + for i in range(0, 6): + with self.subTest(i=i): + self.assertEqual(i % 2, 0) diff --git a/python_files/tests/unittestadapter/.data/test_two_classes.py b/python_files/tests/unittestadapter/.data/test_two_classes.py new file mode 100644 index 000000000000..60b26706ad42 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/test_two_classes.py @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + +# Test class which runs for the test_multiple_ids_run test with the two class parameters. +# Both test functions will be returned in a dictionary with a "success" status, +# and the two tests with their outcome as "success". + + +class ClassOne(unittest.TestCase): + + def test_one(self) -> None: + self.assertGreater(2, 1) + +class ClassTwo(unittest.TestCase): + + def test_two(self) -> None: + self.assertGreater(2, 1) + diff --git a/python_files/tests/unittestadapter/.data/two_patterns/pattern_a_test.py b/python_files/tests/unittestadapter/.data/two_patterns/pattern_a_test.py new file mode 100644 index 000000000000..52641360b526 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/two_patterns/pattern_a_test.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +# Test class for the two file pattern test. It is pattern *test.py. +# The test_ids_multiple_runs function should return a dictionary with a "success" status, +# and the two tests with their outcome as "success". + + +class DiscoveryA(unittest.TestCase): + """Test class for the two file pattern test. It is pattern *test.py + + The test_ids_multiple_runs function should return a dictionary with a "success" status, + and the two tests with their outcome as "success". + """ + + def test_one_a(self) -> None: + self.assertGreater(2, 1) + + def test_two_a(self) -> None: + self.assertNotEqual(2, 1) diff --git a/python_files/tests/unittestadapter/.data/two_patterns/test_pattern_b.py b/python_files/tests/unittestadapter/.data/two_patterns/test_pattern_b.py new file mode 100644 index 000000000000..06b6a818537d --- /dev/null +++ b/python_files/tests/unittestadapter/.data/two_patterns/test_pattern_b.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +# Test class for the two file pattern test. This file is pattern test*.py. +# The test_ids_multiple_runs function should return a dictionary with a "success" status, +# and the two tests with their outcome as "success". + + +class DiscoveryB(unittest.TestCase): + def test_one_b(self) -> None: + self.assertGreater(2, 1) + + def test_two_b(self) -> None: + self.assertNotEqual(2, 1) diff --git a/python_files/tests/unittestadapter/.data/unittest_folder/test_add.py b/python_files/tests/unittestadapter/.data/unittest_folder/test_add.py new file mode 100644 index 000000000000..f562474b596a --- /dev/null +++ b/python_files/tests/unittestadapter/.data/unittest_folder/test_add.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +# Test class which runs for the test_multiple_ids_run test with the two test +# files in the same folder. The cwd is set to the parent folder. This should return +# a dictionary with a "success" status and the two tests with their outcome as "success". + + +def add(a, b): + return a + b + + +class TestAddFunction(unittest.TestCase): + def test_add_positive_numbers(self): + result = add(2, 3) + self.assertEqual(result, 5) + + def test_add_negative_numbers(self): + result = add(-2, -3) + self.assertEqual(result, -5) diff --git a/python_files/tests/unittestadapter/.data/unittest_folder/test_subtract.py b/python_files/tests/unittestadapter/.data/unittest_folder/test_subtract.py new file mode 100644 index 000000000000..8ac3988a3251 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/unittest_folder/test_subtract.py @@ -0,0 +1,21 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + +# Test class which runs for the test_multiple_ids_run test with the two test +# files in the same folder. The cwd is set to the parent folder. This should return +# a dictionary with a "success" status and the two tests with their outcome as "success". + + +def subtract(a, b): + return a - b + + +class TestSubtractFunction(unittest.TestCase): + def test_subtract_positive_numbers(self): + result = subtract(5, 3) + self.assertEqual(result, 2) + + def test_subtract_negative_numbers(self): + result = subtract(-2, -3) + self.assertEqual(result, 1) diff --git a/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py b/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py new file mode 100644 index 000000000000..927a56bc920b --- /dev/null +++ b/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_file.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from unittest import SkipTest + +raise SkipTest("This is unittest.SkipTest calling") + + +def test_example(): + assert 1 == 1 diff --git a/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py b/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py new file mode 100644 index 000000000000..59e66e9a1d40 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/unittest_skip/unittest_skip_function.py @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + + +def add(x, y): + return x + y + + +class SimpleTest(unittest.TestCase): + @unittest.skip("demonstrating skipping") + def testadd1(self): + self.assertEquals(add(4, 5), 9) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/test/pythonFiles/autoimport/two/__init__.py b/python_files/tests/unittestadapter/.data/utils_complex_tree/__init__.py similarity index 100% rename from src/test/pythonFiles/autoimport/two/__init__.py rename to python_files/tests/unittestadapter/.data/utils_complex_tree/__init__.py diff --git a/src/test/pythonFiles/definition/navigation/__init__.py b/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/__init__.py similarity index 100% rename from src/test/pythonFiles/definition/navigation/__init__.py rename to python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/__init__.py diff --git a/src/test/pythonFiles/testFiles/multi/tests/more_tests/__init__.py b/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/__init__.py similarity index 100% rename from src/test/pythonFiles/testFiles/multi/tests/more_tests/__init__.py rename to python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/__init__.py diff --git a/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/test_utils_complex_tree.py b/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/test_utils_complex_tree.py new file mode 100644 index 000000000000..8f57fb880ff1 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/utils_complex_tree/test_outer_folder/test_inner_folder/test_utils_complex_tree.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest + + +class TreeOne(unittest.TestCase): + def test_one(self): + assert True diff --git a/python_files/tests/unittestadapter/.data/utils_decorated_tree.py b/python_files/tests/unittestadapter/.data/utils_decorated_tree.py new file mode 100644 index 000000000000..90fdfc89a27b --- /dev/null +++ b/python_files/tests/unittestadapter/.data/utils_decorated_tree.py @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest +from functools import wraps + + +def my_decorator(f): + @wraps(f) + def wrapper(*args, **kwds): + print("Calling decorated function") + return f(*args, **kwds) + + return wrapper + + +class TreeOne(unittest.TestCase): + """Test class for the test_build_decorated_tree test. + + build_test_tree should build a test tree with these test cases. + """ + + @my_decorator + def test_one(self) -> None: + self.assertGreater(2, 1) + + @my_decorator + def test_two(self) -> None: + self.assertNotEqual(2, 1) diff --git a/python_files/tests/unittestadapter/.data/utils_nested_cases/file_one.py b/python_files/tests/unittestadapter/.data/utils_nested_cases/file_one.py new file mode 100644 index 000000000000..84f7fefc4ebd --- /dev/null +++ b/python_files/tests/unittestadapter/.data/utils_nested_cases/file_one.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + + +class CaseTwoFileOne(unittest.TestCase): + """Test class for the test_nested_test_cases test. + + get_test_case should return tests from the test suites in this folder. + """ + + def test_one(self) -> None: + self.assertGreater(2, 1) + + def test_two(self) -> None: + self.assertNotEqual(2, 1) diff --git a/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/__init__.py b/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/file_two.py b/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/file_two.py new file mode 100644 index 000000000000..235a104016a3 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/utils_nested_cases/folder/file_two.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + + +class CaseTwoFileTwo(unittest.TestCase): + """Test class for the test_nested_test_cases test. + + get_test_case should return tests from the test suites in this folder. + """ + + def test_one(self) -> None: + self.assertGreater(2, 1) + + def test_two(self) -> None: + self.assertNotEqual(2, 1) diff --git a/python_files/tests/unittestadapter/.data/utils_simple_cases.py b/python_files/tests/unittestadapter/.data/utils_simple_cases.py new file mode 100644 index 000000000000..fb3ae7eb7909 --- /dev/null +++ b/python_files/tests/unittestadapter/.data/utils_simple_cases.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + + +class CaseOne(unittest.TestCase): + """Test class for the test_simple_test_cases test. + + get_test_case should return tests from the test suite. + """ + + def test_one(self) -> None: + self.assertGreater(2, 1) + + def test_two(self) -> None: + self.assertNotEqual(2, 1) diff --git a/python_files/tests/unittestadapter/.data/utils_simple_tree.py b/python_files/tests/unittestadapter/.data/utils_simple_tree.py new file mode 100644 index 000000000000..6db51a4fd80b --- /dev/null +++ b/python_files/tests/unittestadapter/.data/utils_simple_tree.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import unittest + + +class TreeOne(unittest.TestCase): + """Test class for the test_build_simple_tree test. + + build_test_tree should build a test tree with these test cases. + """ + + def test_one(self) -> None: + self.assertGreater(2, 1) + + def test_two(self) -> None: + self.assertNotEqual(2, 1) diff --git a/python_files/tests/unittestadapter/__init__.py b/python_files/tests/unittestadapter/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/conftest.py b/python_files/tests/unittestadapter/conftest.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/tests/unittestadapter/conftest.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/tests/unittestadapter/django_test_execution_script.py b/python_files/tests/unittestadapter/django_test_execution_script.py new file mode 100644 index 000000000000..21dd945224ea --- /dev/null +++ b/python_files/tests/unittestadapter/django_test_execution_script.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys + +sys.path.append(os.fspath(pathlib.Path(__file__).parent.parent)) + +from unittestadapter.django_handler import django_execution_runner + +if __name__ == "__main__": + args = sys.argv[1:] + manage_py_path = args[0] + test_ids = args[1:] + # currently doesn't support additional args past test_ids. + django_execution_runner(manage_py_path, test_ids, []) diff --git a/python_files/tests/unittestadapter/expected_discovery_test_output.py b/python_files/tests/unittestadapter/expected_discovery_test_output.py new file mode 100644 index 000000000000..0901f21bfbc2 --- /dev/null +++ b/python_files/tests/unittestadapter/expected_discovery_test_output.py @@ -0,0 +1,171 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib + +from unittestadapter.pvsc_utils import TestNodeTypeEnum + +TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" + + +def find_class_line_number(class_name: str, test_file_path) -> str: + """Function which finds the correct line number for a class definition. + + Args: + class_name: The name of the class to find the line number for. + test_file_path: The path to the test file where the class is located. + """ + # Look for the class definition line + with pathlib.Path(test_file_path).open() as f: + for i, line in enumerate(f): + # Match "class ClassName" or "class ClassName(" or "class ClassName:" + if line.strip().startswith(f"class {class_name}") or line.strip().startswith( + f"class {class_name}(" + ): + return str(i + 1) + error_str: str = f"Class {class_name!r} not found on any line in {test_file_path}" + raise ValueError(error_str) + + +skip_unittest_folder_discovery_output = { + "path": os.fspath(TEST_DATA_PATH / "unittest_skip"), + "name": "unittest_skip", + "type_": TestNodeTypeEnum.folder, + "children": [ + { + "path": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_file.py"), + "name": "unittest_skip_file.py", + "type_": TestNodeTypeEnum.file, + "children": [], + "id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_file.py"), + }, + { + "path": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py"), + "name": "unittest_skip_function.py", + "type_": TestNodeTypeEnum.file, + "children": [ + { + "path": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" + ), + "name": "SimpleTest", + "type_": TestNodeTypeEnum.class_, + "children": [ + { + "name": "testadd1", + "path": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" + ), + "lineno": "13", + "type_": TestNodeTypeEnum.test, + "id_": os.fspath( + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py" + ) + + "\\SimpleTest\\testadd1", + "runID": "unittest_skip_function.SimpleTest.testadd1", + } + ], + "id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py") + + "\\SimpleTest", + "lineno": find_class_line_number( + "SimpleTest", + TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py", + ), + } + ], + "id_": os.fspath(TEST_DATA_PATH / "unittest_skip" / "unittest_skip_function.py"), + }, + ], + "id_": os.fspath(TEST_DATA_PATH / "unittest_skip"), +} + +complex_tree_file_path = os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + "test_utils_complex_tree.py", + ) +) +complex_tree_expected_output = { + "name": "utils_complex_tree", + "type_": TestNodeTypeEnum.folder, + "path": os.fsdecode(pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree")), + "children": [ + { + "name": "test_outer_folder", + "type_": TestNodeTypeEnum.folder, + "path": os.fsdecode( + pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree", "test_outer_folder") + ), + "children": [ + { + "name": "test_inner_folder", + "type_": TestNodeTypeEnum.folder, + "path": os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + ) + ), + "children": [ + { + "name": "test_utils_complex_tree.py", + "type_": TestNodeTypeEnum.file, + "path": complex_tree_file_path, + "children": [ + { + "name": "TreeOne", + "type_": TestNodeTypeEnum.class_, + "path": complex_tree_file_path, + "children": [ + { + "name": "test_one", + "type_": TestNodeTypeEnum.test, + "path": complex_tree_file_path, + "lineno": "7", + "id_": complex_tree_file_path + + "\\" + + "TreeOne" + + "\\" + + "test_one", + "runID": "utils_complex_tree.test_outer_folder.test_inner_folder.test_utils_complex_tree.TreeOne.test_one", + }, + ], + "id_": complex_tree_file_path + "\\" + "TreeOne", + "lineno": find_class_line_number( + "TreeOne", + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + "test_utils_complex_tree.py", + ), + ), + } + ], + "id_": complex_tree_file_path, + } + ], + "id_": os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + ) + ), + }, + ], + "id_": os.fsdecode( + pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree", "test_outer_folder") + ), + } + ], + "id_": os.fsdecode(pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree")), +} diff --git a/python_files/tests/unittestadapter/test_coverage.py b/python_files/tests/unittestadapter/test_coverage.py new file mode 100644 index 000000000000..76fdfec43376 --- /dev/null +++ b/python_files/tests/unittestadapter/test_coverage.py @@ -0,0 +1,106 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys + +import coverage +import pytest +from packaging.version import Version + +sys.path.append(os.fspath(pathlib.Path(__file__).parent)) + +python_files_path = pathlib.Path(__file__).parent.parent.parent +sys.path.insert(0, os.fspath(python_files_path)) +sys.path.insert(0, os.fspath(python_files_path / "lib" / "python")) + +from tests.pytestadapter import helpers # noqa: E402 + +TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" + + +def test_basic_coverage(): + """This test runs on a simple django project with three tests, two of which pass and one that fails.""" + coverage_ex_folder: pathlib.Path = TEST_DATA_PATH / "coverage_ex" + execution_script: pathlib.Path = python_files_path / "unittestadapter" / "execution.py" + test_ids = [ + "test_reverse.TestReverseFunctions.test_reverse_sentence", + "test_reverse.TestReverseFunctions.test_reverse_sentence_error", + "test_reverse.TestReverseFunctions.test_reverse_string", + ] + argv = [os.fsdecode(execution_script), "--udiscovery", "-vv", "-s", ".", "-p", "*test*.py"] + argv = argv + test_ids + + actual = helpers.runner_with_cwd_env( + argv, + coverage_ex_folder, + {"COVERAGE_ENABLED": os.fspath(coverage_ex_folder), "_TEST_VAR_UNITTEST": "True"}, + ) + + assert actual + cov = actual[-1] + assert cov + results = cov["result"] + assert results + assert len(results) == 3 + focal_function_coverage = results.get(os.fspath(TEST_DATA_PATH / "coverage_ex" / "reverse.py")) + assert focal_function_coverage + assert focal_function_coverage.get("lines_covered") is not None + assert focal_function_coverage.get("lines_missed") is not None + assert set(focal_function_coverage.get("lines_covered")) == {4, 5, 7, 9, 10, 11, 12, 13, 14} + assert set(focal_function_coverage.get("lines_missed")) == {6} + coverage_version = Version(coverage.__version__) + # only include check for branches if the version is >= 7.7.0 + if coverage_version >= Version("7.7.0"): + assert focal_function_coverage.get("executed_branches") == 3 + assert focal_function_coverage.get("total_branches") == 4 + + +@pytest.mark.parametrize("manage_py_file", ["manage.py", "old_manage.py"]) +@pytest.mark.timeout(30) +def test_basic_django_coverage(manage_py_file): + """This test validates that the coverage is correctly calculated for a Django project.""" + data_path: pathlib.Path = TEST_DATA_PATH / "simple_django" + manage_py_path: str = os.fsdecode(data_path / manage_py_file) + execution_script: pathlib.Path = python_files_path / "unittestadapter" / "execution.py" + + test_ids = [ + "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question", + "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question_2", + "polls.tests.QuestionModelTests.test_question_creation_and_retrieval", + ] + + script_str = os.fsdecode(execution_script) + actual = helpers.runner_with_cwd_env( + [script_str, "--udiscovery", "-p", "*test*.py", *test_ids], + data_path, + { + "MANAGE_PY_PATH": manage_py_path, + "_TEST_VAR_UNITTEST": "True", + "COVERAGE_ENABLED": os.fspath(data_path), + }, + ) + + assert actual + cov = actual[-1] + assert cov + results = cov["result"] + assert results + assert len(results) == 16 + polls_views_coverage = results.get(str(data_path / "polls" / "views.py")) + assert polls_views_coverage + assert polls_views_coverage.get("lines_covered") is not None + assert polls_views_coverage.get("lines_missed") is not None + assert set(polls_views_coverage.get("lines_covered")) == {3, 4, 6} + assert set(polls_views_coverage.get("lines_missed")) == {7} + + model_cov = results.get(str(data_path / "polls" / "models.py")) + coverage_version = Version(coverage.__version__) + # only include check for branches if the version is >= 7.7.0 + if coverage_version >= Version("7.7.0"): + assert model_cov.get("executed_branches") == 1 + assert model_cov.get("total_branches") == 2 diff --git a/python_files/tests/unittestadapter/test_discovery.py b/python_files/tests/unittestadapter/test_discovery.py new file mode 100644 index 000000000000..ab028ef176c3 --- /dev/null +++ b/python_files/tests/unittestadapter/test_discovery.py @@ -0,0 +1,447 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys +from typing import Any, Dict, List + +import pytest + +from unittestadapter.discovery import discover_tests +from unittestadapter.pvsc_utils import TestNodeTypeEnum, parse_unittest_args + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) + +from tests.pytestadapter import helpers # noqa: E402 +from tests.tree_comparison_helper import is_same_tree # noqa: E402 + +from . import expected_discovery_test_output # noqa: E402 + +TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" + + +@pytest.mark.parametrize( + ("args", "expected"), + [ + ( + ["-s", "something", "-p", "other*", "-t", "else"], + ("something", "other*", "else", 1, None, None), + ), + ( + [ + "--start-directory", + "foo", + "--pattern", + "bar*", + "--top-level-directory", + "baz", + ], + ("foo", "bar*", "baz", 1, None, None), + ), + ( + ["--foo", "something"], + (".", "test*.py", None, 1, None, None), + ), + ( + ["--foo", "something", "-v"], + (".", "test*.py", None, 2, None, None), + ), + ( + ["--foo", "something", "-f"], + (".", "test*.py", None, 1, True, None), + ), + ( + ["--foo", "something", "--verbose", "-f"], + (".", "test*.py", None, 2, True, None), + ), + ( + ["--foo", "something", "-q", "--failfast"], + (".", "test*.py", None, 0, True, None), + ), + ( + ["--foo", "something", "--quiet"], + (".", "test*.py", None, 0, None, None), + ), + ( + ["--foo", "something", "--quiet", "--locals"], + (".", "test*.py", None, 0, None, True), + ), + ], +) +def test_parse_unittest_args(args: List[str], expected: List[str]) -> None: + """The parse_unittest_args function should return values for the start_dir, pattern, and top_level_dir arguments when passed as command-line options, and ignore unrecognized arguments.""" + actual = parse_unittest_args(args) + + assert actual == expected + + +def test_simple_discovery() -> None: + """The discover_tests function should return a dictionary with a "success" status, no errors, and a test tree if unittest discovery was performed successfully.""" + start_dir = os.fsdecode(TEST_DATA_PATH) + pattern = "discovery_simple*" + file_path = os.fsdecode(pathlib.PurePath(TEST_DATA_PATH / "discovery_simple.py")) + + expected = { + "path": start_dir, + "type_": TestNodeTypeEnum.folder, + "name": ".data", + "children": [ + { + "name": "discovery_simple.py", + "type_": TestNodeTypeEnum.file, + "path": file_path, + "children": [ + { + "name": "DiscoverySimple", + "path": file_path, + "type_": TestNodeTypeEnum.class_, + "children": [ + { + "name": "test_one", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "14", + "id_": file_path + "\\" + "DiscoverySimple" + "\\" + "test_one", + }, + { + "name": "test_two", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "17", + "id_": file_path + "\\" + "DiscoverySimple" + "\\" + "test_two", + }, + ], + "id_": file_path + "\\" + "DiscoverySimple", + } + ], + "id_": file_path, + } + ], + "id_": start_dir, + } + + actual = discover_tests(start_dir, pattern, None) + + assert actual["status"] == "success" + assert is_same_tree(actual.get("tests"), expected, ["id_", "lineno", "name"]) + assert "error" not in actual + + +def test_simple_discovery_with_top_dir_calculated() -> None: + """The discover_tests function should return a dictionary with a "success" status, no errors, and a test tree if unittest discovery was performed successfully.""" + start_dir = "." + pattern = "discovery_simple*" + file_path = os.fsdecode(pathlib.PurePath(TEST_DATA_PATH / "discovery_simple.py")) + + expected = { + "path": os.fsdecode(pathlib.PurePath(TEST_DATA_PATH)), + "type_": TestNodeTypeEnum.folder, + "name": ".data", + "children": [ + { + "name": "discovery_simple.py", + "type_": TestNodeTypeEnum.file, + "path": file_path, + "children": [ + { + "name": "DiscoverySimple", + "path": file_path, + "type_": TestNodeTypeEnum.class_, + "children": [ + { + "name": "test_one", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "14", + "id_": file_path + "\\" + "DiscoverySimple" + "\\" + "test_one", + }, + { + "name": "test_two", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "17", + "id_": file_path + "\\" + "DiscoverySimple" + "\\" + "test_two", + }, + ], + "id_": file_path + "\\" + "DiscoverySimple", + } + ], + "id_": file_path, + } + ], + "id_": os.fsdecode(pathlib.PurePath(TEST_DATA_PATH)), + } + + # Define the CWD to be the root of the test data folder. + os.chdir(os.fsdecode(pathlib.PurePath(TEST_DATA_PATH))) + actual = discover_tests(start_dir, pattern, None) + + assert actual["status"] == "success" + assert is_same_tree(actual.get("tests"), expected, ["id_", "lineno", "name"]) + assert "error" not in actual + + +def test_empty_discovery() -> None: + """The discover_tests function should return a dictionary with a "success" status, no errors, and no test tree if unittest discovery was performed successfully but no tests were found.""" + start_dir = os.fsdecode(TEST_DATA_PATH) + pattern = "discovery_empty*" + + actual = discover_tests(start_dir, pattern, None) + + assert actual["status"] == "success" + assert "tests" in actual + assert "error" not in actual + + +def test_error_discovery() -> None: + """The discover_tests function should return a dictionary with an "error" status, the discovered tests, and a list of errors if unittest discovery failed at some point.""" + # Discover tests in .data/discovery_error/. + start_path = pathlib.PurePath(TEST_DATA_PATH / "discovery_error") + start_dir = os.fsdecode(start_path) + pattern = "file*" + + file_path = os.fsdecode(start_path / "file_two.py") + + expected = { + "path": start_dir, + "type_": TestNodeTypeEnum.folder, + "name": "discovery_error", + "children": [ + { + "name": "file_two.py", + "type_": TestNodeTypeEnum.file, + "path": file_path, + "children": [ + { + "name": "DiscoveryErrorTwo", + "path": file_path, + "type_": TestNodeTypeEnum.class_, + "children": [ + { + "name": "test_one", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "14", + "id_": file_path + "\\" + "DiscoveryErrorTwo" + "\\" + "test_one", + }, + { + "name": "test_two", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "17", + "id_": file_path + "\\" + "DiscoveryErrorTwo" + "\\" + "test_two", + }, + ], + "id_": file_path + "\\" + "DiscoveryErrorTwo", + } + ], + "id_": file_path, + } + ], + "id_": start_dir, + } + + actual = discover_tests(start_dir, pattern, None) + + assert actual["status"] == "error" + assert is_same_tree(expected, actual.get("tests"), ["id_", "lineno", "name"]) + assert len(actual.get("error", [])) == 1 + + +def test_unit_skip() -> None: + """The discover_tests function should return a dictionary with a "success" status, no errors, and test tree. + + if unittest discovery was performed and found a test in one file marked as skipped and another file marked as skipped. + """ + start_dir = os.fsdecode(TEST_DATA_PATH / "unittest_skip") + pattern = "unittest_*" + + actual = discover_tests(start_dir, pattern, None) + + assert actual["status"] == "success" + assert "tests" in actual + assert is_same_tree( + actual.get("tests"), + expected_discovery_test_output.skip_unittest_folder_discovery_output, + ["id_", "lineno", "name"], + ) + assert "error" not in actual + + +def test_complex_tree() -> None: + """This test specifically tests when different start_dir and top_level_dir are provided.""" + start_dir = os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + ) + ) + pattern = "test_*.py" + top_level_dir = os.fsdecode(pathlib.PurePath(TEST_DATA_PATH, "utils_complex_tree")) + actual = discover_tests(start_dir, pattern, top_level_dir) + assert actual["status"] == "success" + assert "error" not in actual + assert is_same_tree( + actual.get("tests"), + expected_discovery_test_output.complex_tree_expected_output, + ["id_", "lineno", "name"], + ) + + +def test_simple_django_collect(): + test_data_path: pathlib.Path = pathlib.Path(__file__).parent / ".data" + python_files_path: pathlib.Path = pathlib.Path(__file__).parent.parent.parent + discovery_script_path: str = os.fsdecode(python_files_path / "unittestadapter" / "discovery.py") + data_path: pathlib.Path = test_data_path / "simple_django" + manage_py_path: str = os.fsdecode(pathlib.Path(data_path, "manage.py")) + + actual = helpers.runner_with_cwd_env( + [ + discovery_script_path, + "--udiscovery", + ], + data_path, + {"MANAGE_PY_PATH": manage_py_path}, + ) + + assert actual + actual_list: List[Dict[str, Any]] = actual + assert actual_list is not None + if actual_list is not None: + actual_item = actual_list.pop(0) + assert all(item in actual_item for item in ("status", "cwd")) + assert actual_item.get("status") == "success", ( + f"Status is not 'success', error is: {actual_item.get('error')}" + ) + assert actual_item.get("cwd") == os.fspath(data_path) + assert len(actual_item["tests"]["children"]) == 1 + assert actual_item["tests"]["children"][0]["children"][0]["id_"] == os.fsdecode( + pathlib.PurePath(test_data_path, "simple_django", "polls", "tests.py") + ) + assert ( + len(actual_item["tests"]["children"][0]["children"][0]["children"][0]["children"]) == 3 + ) + + +def test_project_root_path_with_cwd_override() -> None: + """Test unittest discovery with project_root_path parameter. + + This simulates project-based testing where the cwd in the payload should be + the project root (project_root_path) rather than the start_dir. + + When project_root_path is provided: + - The cwd in the response should match project_root_path + - The test tree root should still be built correctly based on top_level_dir + """ + # Use unittest_skip folder as our "project" directory + project_path = TEST_DATA_PATH / "unittest_skip" + start_dir = os.fsdecode(project_path) + pattern = "unittest_*" + + # Call discover_tests with project_root_path to simulate PROJECT_ROOT_PATH + actual = discover_tests(start_dir, pattern, None, project_root_path=start_dir) + + assert actual["status"] == "success" + # cwd in response should match the project_root_path (project root) + assert actual["cwd"] == os.fsdecode(project_path), ( + f"Expected cwd '{os.fsdecode(project_path)}', got '{actual['cwd']}'" + ) + assert "tests" in actual + # Verify the test tree structure matches expected output + assert is_same_tree( + actual.get("tests"), + expected_discovery_test_output.skip_unittest_folder_discovery_output, + ["id_", "lineno", "name"], + ) + assert "error" not in actual + + +def test_project_root_path_with_different_cwd_and_start_dir() -> None: + """Test unittest discovery where project_root_path differs from start_dir. + + This simulates the scenario where: + - start_dir points to a subfolder where tests are located + - project_root_path (PROJECT_ROOT_PATH) points to the project root + + The cwd in the response should be the project root, while discovery + still runs from the start_dir. + """ + # Use utils_complex_tree as our test case - discovery from a subfolder + project_path = TEST_DATA_PATH / "utils_complex_tree" + start_dir = os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + ) + ) + pattern = "test_*.py" + top_level_dir = os.fsdecode(project_path) + + # Call discover_tests with project_root_path set to project root + actual = discover_tests(start_dir, pattern, top_level_dir, project_root_path=top_level_dir) + + assert actual["status"] == "success" + # cwd should be the project root (project_root_path), not the start_dir + assert actual["cwd"] == os.fsdecode(project_path), ( + f"Expected cwd '{os.fsdecode(project_path)}', got '{actual['cwd']}'" + ) + assert "error" not in actual + # Test tree should still be structured correctly with top_level_dir as root + assert is_same_tree( + actual.get("tests"), + expected_discovery_test_output.complex_tree_expected_output, + ["id_", "lineno", "name"], + ) + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="Symlinks require elevated privileges on Windows", +) +def test_symlink_with_project_root_path() -> None: + """Test unittest discovery with both symlink and PROJECT_ROOT_PATH set. + + This tests the combination of: + 1. A symlinked test directory + 2. project_root_path (PROJECT_ROOT_PATH) set to the symlink path + + This simulates project-based testing where the project root is a symlink, + ensuring test IDs and paths are correctly resolved through the symlink. + """ + with helpers.create_symlink(TEST_DATA_PATH, "unittest_skip", "symlink_unittest") as ( + _source, + destination, + ): + assert destination.is_symlink() + + # Run discovery with: + # - start_dir pointing to the symlink destination + # - project_root_path set to the symlink destination (simulating PROJECT_ROOT_PATH) + start_dir = os.fsdecode(destination) + pattern = "unittest_*" + + actual = discover_tests(start_dir, pattern, None, project_root_path=start_dir) + + assert actual["status"] == "success", ( + f"Status is not 'success', error is: {actual.get('error')}" + ) + # cwd should be the symlink path (project_root_path) + assert actual["cwd"] == os.fsdecode(destination), ( + f"CWD does not match symlink path: expected {os.fsdecode(destination)}, got {actual['cwd']}" + ) + assert "tests" in actual + assert actual["tests"] is not None + # The test tree root should be named after the symlink directory + assert actual["tests"]["name"] == "symlink_unittest", ( + f"Expected root name 'symlink_unittest', got '{actual['tests']['name']}'" + ) + # The test tree root path should use the symlink path + assert actual["tests"]["path"] == os.fsdecode(destination), ( + f"Expected root path to be symlink, got '{actual['tests']['path']}'" + ) diff --git a/python_files/tests/unittestadapter/test_execution.py b/python_files/tests/unittestadapter/test_execution.py new file mode 100644 index 000000000000..cab03f0b5dc4 --- /dev/null +++ b/python_files/tests/unittestadapter/test_execution.py @@ -0,0 +1,474 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys +from typing import TYPE_CHECKING, Any, Dict, List, Optional +from unittest.mock import patch + +import pytest + +sys.path.append(os.fspath(pathlib.Path(__file__).parent)) + +python_files_path = pathlib.Path(__file__).parent.parent.parent +sys.path.insert(0, os.fspath(python_files_path)) +sys.path.insert(0, os.fspath(python_files_path / "lib" / "python")) + +from tests.pytestadapter import helpers # noqa: E402 +from unittestadapter.execution import run_tests # noqa: E402 + +if TYPE_CHECKING: + from unittestadapter.pvsc_utils import ExecutionPayloadDict + +TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" + + +def test_no_ids_run() -> None: + """This test runs on an empty array of test_ids, therefore it should return an empty dict for the result.""" + start_dir: str = os.fspath(TEST_DATA_PATH) + testids = [] + pattern = "discovery_simple*" + actual = run_tests(start_dir, testids, pattern, None, 1, None) + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + if actual["result"] is not None: + assert len(actual["result"]) == 0 + else: + raise AssertionError("actual['result'] is None") + + +@pytest.fixture +def mock_send_run_data(): + with patch("unittestadapter.execution.send_run_data") as mock: + yield mock + + +def test_single_ids_run(mock_send_run_data): + """This test runs on a single test_id, therefore it should return a dict with a single key-value pair for the result. + + This single test passes so the outcome should be 'success'. + """ + id_ = "discovery_simple.DiscoverySimple.test_one" + os.environ["TEST_RUN_PIPE"] = "fake" + actual: ExecutionPayloadDict = run_tests( + os.fspath(TEST_DATA_PATH), + [id_], + "discovery_simple*", + None, + 1, + None, + ) + + # Access the arguments + args, _ = mock_send_run_data.call_args + test_actual = args[0] # first argument is the result + + assert test_actual + actual_result: Optional[Dict[str, Dict[str, Optional[str]]]] = actual["result"] + if actual_result is None: + raise AssertionError("actual_result is None") + else: + if not isinstance(actual_result, Dict): + raise AssertionError("actual_result is not a Dict") + assert len(actual_result) == 1 + assert id_ in actual_result + id_result = actual_result[id_] + assert id_result is not None + assert "outcome" in id_result + assert id_result["outcome"] == "success" + + +def test_subtest_run(mock_send_run_data) -> None: # noqa: ARG001 + """This test runs on a the test_subtest which has a single method, test_even, that uses unittest subtest. + + The actual result of run should return a dict payload with 6 entry for the 6 subtests. + """ + id_ = "test_subtest.NumbersTest.test_even" + os.environ["TEST_RUN_PIPE"] = "fake" + actual = run_tests( + os.fspath(TEST_DATA_PATH), + [id_], + "test_subtest.py", + None, + 1, + None, + ) + subtests_ids = [ + "test_subtest.NumbersTest.test_even (i=0)", + "test_subtest.NumbersTest.test_even (i=1)", + "test_subtest.NumbersTest.test_even (i=2)", + "test_subtest.NumbersTest.test_even (i=3)", + "test_subtest.NumbersTest.test_even (i=4)", + "test_subtest.NumbersTest.test_even (i=5)", + ] + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert actual["result"] is not None + result = actual["result"] + assert len(result) == 6 + for id_ in subtests_ids: + assert id_ in result + + +@pytest.mark.parametrize( + ("test_ids", "pattern", "cwd", "expected_outcome"), + [ + ( + [ + "test_add.TestAddFunction.test_add_negative_numbers", + "test_add.TestAddFunction.test_add_positive_numbers", + ], + "test_add.py", + os.fspath(TEST_DATA_PATH / "unittest_folder"), + "success", + ), + ( + [ + "test_add.TestAddFunction.test_add_negative_numbers", + "test_add.TestAddFunction.test_add_positive_numbers", + "test_subtract.TestSubtractFunction.test_subtract_negative_numbers", + "test_subtract.TestSubtractFunction.test_subtract_positive_numbers", + ], + "test*", + os.fspath(TEST_DATA_PATH / "unittest_folder"), + "success", + ), + ( + [ + "pattern_a_test.DiscoveryA.test_one_a", + "pattern_a_test.DiscoveryA.test_two_a", + ], + "*test.py", + os.fspath(TEST_DATA_PATH / "two_patterns"), + "success", + ), + ( + [ + "test_pattern_b.DiscoveryB.test_one_b", + "test_pattern_b.DiscoveryB.test_two_b", + ], + "test_*", + os.fspath(TEST_DATA_PATH / "two_patterns"), + "success", + ), + ( + [ + "file_one.CaseTwoFileOne.test_one", + "file_one.CaseTwoFileOne.test_two", + "folder.file_two.CaseTwoFileTwo.test_one", + "folder.file_two.CaseTwoFileTwo.test_two", + ], + "*", + os.fspath(TEST_DATA_PATH / "utils_nested_cases"), + "success", + ), + ( + [ + "test_two_classes.ClassOne.test_one", + "test_two_classes.ClassTwo.test_two", + ], + "test_two_classes.py", + os.fspath(TEST_DATA_PATH), + "success", + ), + ( + [ + "test_scene.TestMathOperations.test_operations(add)", + "test_scene.TestMathOperations.test_operations(subtract)", + "test_scene.TestMathOperations.test_operations(multiply)", + ], + "*", + os.fspath(TEST_DATA_PATH / "test_scenarios" / "tests"), + "success", + ), + ], +) +def test_multiple_ids_run(mock_send_run_data, test_ids, pattern, cwd, expected_outcome) -> None: # noqa: ARG001 + """ + The following are all successful tests of different formats. + + # 1. Two tests with the `pattern` specified as a file + # 2. Two test files in the same folder called `unittest_folder` + # 3. A folder with two different test file patterns, this test gathers pattern `*test` + # 4. A folder with two different test file patterns, this test gathers pattern `test_*` + # 5. A nested structure where a test file is on the same level as a folder containing a test file + # 6. Test file with two test classes + + All tests should have the outcome of `success`. + """ + os.environ["TEST_RUN_PIPE"] = "fake" + actual = run_tests(cwd, test_ids, pattern, None, 1, None) + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == cwd + assert actual["result"] is not None + result = actual["result"] + assert len(result) == len(test_ids) + for test_id in test_ids: + assert test_id in result + id_result = result[test_id] + assert id_result is not None + assert "outcome" in id_result + assert id_result["outcome"] == expected_outcome + assert True + + +def test_failed_tests(mock_send_run_data): # noqa: ARG001 + """This test runs on a single file `test_fail` with two tests that fail.""" + os.environ["TEST_RUN_PIPE"] = "fake" + test_ids = [ + "test_fail_simple.RunFailSimple.test_one_fail", + "test_fail_simple.RunFailSimple.test_two_fail", + ] + actual = run_tests( + os.fspath(TEST_DATA_PATH), + test_ids, + "test_fail_simple*", + None, + 1, + None, + ) + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert actual["result"] is not None + result = actual["result"] + assert len(result) == len(test_ids) + for test_id in test_ids: + assert test_id in result + id_result = result[test_id] + assert id_result is not None + assert "outcome" in id_result + assert id_result["outcome"] == "failure" + assert "message" in id_result + assert "traceback" in id_result + assert "2 not greater than 3" in str(id_result["message"]) or "1 == 1" in str( + id_result["traceback"] + ) + assert True + + +def test_unknown_id(mock_send_run_data): # noqa: ARG001 + """This test runs on a unknown test_id, therefore it should return an error as the outcome as it attempts to find the given test.""" + os.environ["TEST_RUN_PIPE"] = "fake" + test_ids = ["unknown_id"] + actual = run_tests( + os.fspath(TEST_DATA_PATH), + test_ids, + "test_fail_simple*", + None, + 1, + None, + ) + assert actual + assert all(item in actual for item in ("cwd", "status")) + assert actual["status"] == "success" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH) + assert actual["result"] is not None + result = actual["result"] + assert len(result) == len(test_ids) + assert "unittest.loader._FailedTest.unknown_id" in result + id_result = result["unittest.loader._FailedTest.unknown_id"] + assert id_result is not None + assert "outcome" in id_result + assert id_result["outcome"] == "error" + assert "message" in id_result + assert "traceback" in id_result + + +def test_incorrect_path(): + """This test runs on a non existent path, therefore it should return an error as the outcome as it attempts to find the given folder.""" + test_ids = ["unknown_id"] + os.environ["TEST_RUN_PIPE"] = "fake" + + actual = run_tests( + os.fspath(TEST_DATA_PATH / "unknown_folder"), + test_ids, + "test_fail_simple*", + None, + 1, + None, + ) + assert actual + assert all(item in actual for item in ("cwd", "status", "error")) + assert actual["status"] == "error" + assert actual["cwd"] == os.fspath(TEST_DATA_PATH / "unknown_folder") + + +def test_basic_run_django(): + """This test runs on a simple django project with three tests, two of which pass and one that fails.""" + data_path: pathlib.Path = TEST_DATA_PATH / "simple_django" + manage_py_path: str = os.fsdecode(data_path / "manage.py") + execution_script: pathlib.Path = ( + pathlib.Path(__file__).parent / "django_test_execution_script.py" + ) + + test_ids = [ + "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question", + "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question_2", + "polls.tests.QuestionModelTests.test_question_creation_and_retrieval", + ] + script_str = os.fsdecode(execution_script) + actual = helpers.runner_with_cwd_env( + [script_str, manage_py_path, *test_ids], + data_path, + {"MANAGE_PY_PATH": manage_py_path}, + ) + assert actual + actual_list: List[Dict[str, Dict[str, Any]]] = actual + actual_result_dict = {} + assert len(actual_list) == 3 + for actual_item in actual_list: + assert all(item in actual_item for item in ("status", "cwd", "result")) + assert actual_item.get("cwd") == os.fspath(data_path) + actual_result_dict.update(actual_item["result"]) + for test_id in test_ids: + assert test_id in actual_result_dict + id_result = actual_result_dict[test_id] + assert id_result is not None + assert "outcome" in id_result + if ( + test_id + == "polls.tests.QuestionModelTests.test_was_published_recently_with_future_question_2" + ): + assert id_result["outcome"] == "failure" + else: + assert id_result["outcome"] == "success" + + +def test_project_root_path_with_cwd_override(mock_send_run_data) -> None: # noqa: ARG001 + """Test unittest execution with project_root_path parameter. + + This simulates project-based testing where the cwd in the payload should be + the project root (project_root_path) rather than the start_dir. + + When project_root_path is provided: + - The cwd in the response should match project_root_path + - Test execution should still work correctly with start_dir + """ + # Use unittest_folder as our "project" directory + project_path = TEST_DATA_PATH / "unittest_folder" + start_dir = os.fsdecode(project_path) + pattern = "test_add*" + test_ids = [ + "test_add.TestAddFunction.test_add_positive_numbers", + ] + + os.environ["TEST_RUN_PIPE"] = "fake" + + # Call run_tests with project_root_path to simulate PROJECT_ROOT_PATH + actual = run_tests( + start_dir, + test_ids, + pattern, + None, + 1, + None, + project_root_path=start_dir, + ) + + assert actual["status"] == "success" + # cwd in response should match the project_root_path (project root) + assert actual["cwd"] == os.fsdecode(project_path), ( + f"Expected cwd '{os.fsdecode(project_path)}', got '{actual['cwd']}'" + ) + assert actual["result"] is not None + assert test_ids[0] in actual["result"] + assert actual["result"][test_ids[0]]["outcome"] == "success" + + +def test_project_root_path_with_different_cwd_and_start_dir(mock_send_run_data) -> None: # noqa: ARG001 + """Test unittest execution where project_root_path differs from start_dir. + + This simulates the scenario where: + - start_dir points to a subfolder where tests are located + - project_root_path (PROJECT_ROOT_PATH) points to the project root + + The cwd in the response should be the project root, while execution + still runs from the start_dir. + """ + # Use utils_nested_cases as our test case + project_path = TEST_DATA_PATH / "utils_nested_cases" + start_dir = os.fsdecode(project_path) + pattern = "*" + test_ids = [ + "file_one.CaseTwoFileOne.test_one", + ] + + os.environ["TEST_RUN_PIPE"] = "fake" + + # Call run_tests with project_root_path set to project root + actual = run_tests( + start_dir, + test_ids, + pattern, + None, + 1, + None, + project_root_path=os.fsdecode(project_path), + ) + + assert actual["status"] == "success" + # cwd should be the project root (project_root_path) + assert actual["cwd"] == os.fsdecode(project_path), ( + f"Expected cwd '{os.fsdecode(project_path)}', got '{actual['cwd']}'" + ) + assert actual["result"] is not None + assert test_ids[0] in actual["result"] + + +@pytest.mark.skipif( + sys.platform == "win32", + reason="Symlinks require elevated privileges on Windows", +) +def test_symlink_with_project_root_path(mock_send_run_data) -> None: # noqa: ARG001 + """Test unittest execution with both symlink and project_root_path set. + + This tests the combination of: + 1. A symlinked test directory + 2. project_root_path (PROJECT_ROOT_PATH) set to the symlink path + + This simulates project-based testing where the project root is a symlink, + ensuring execution payloads correctly use the symlink path. + """ + with helpers.create_symlink(TEST_DATA_PATH, "unittest_folder", "symlink_unittest_exec") as ( + _source, + destination, + ): + assert destination.is_symlink() + + # Run execution with: + # - start_dir pointing to the symlink destination + # - project_root_path set to the symlink destination (simulating PROJECT_ROOT_PATH) + start_dir = os.fsdecode(destination) + pattern = "test_add*" + test_ids = [ + "test_add.TestAddFunction.test_add_positive_numbers", + ] + + os.environ["TEST_RUN_PIPE"] = "fake" + + actual = run_tests( + start_dir, + test_ids, + pattern, + None, + 1, + None, + project_root_path=start_dir, + ) + + assert actual["status"] == "success", ( + f"Status is not 'success', error is: {actual.get('error')}" + ) + # cwd should be the symlink path (project_root_path) + assert actual["cwd"] == os.fsdecode(destination), ( + f"CWD does not match symlink path: expected {os.fsdecode(destination)}, got {actual['cwd']}" + ) diff --git a/python_files/tests/unittestadapter/test_utils.py b/python_files/tests/unittestadapter/test_utils.py new file mode 100644 index 000000000000..dc8a81175e70 --- /dev/null +++ b/python_files/tests/unittestadapter/test_utils.py @@ -0,0 +1,339 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys +import unittest + +import pytest + +from unittestadapter.pvsc_utils import ( + TestNode, + TestNodeTypeEnum, + build_test_tree, + get_child_node, + get_test_case, +) + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) + +from tests.tree_comparison_helper import is_same_tree # noqa: E402 + +TEST_DATA_PATH = pathlib.Path(__file__).parent / ".data" + + +@pytest.mark.parametrize( + ("directory", "pattern", "expected"), + [ + ( + ".", + "utils_simple_cases*", + [ + "utils_simple_cases.CaseOne.test_one", + "utils_simple_cases.CaseOne.test_two", + ], + ), + ( + "utils_nested_cases", + "file*", + [ + "file_one.CaseTwoFileOne.test_one", + "file_one.CaseTwoFileOne.test_two", + "folder.file_two.CaseTwoFileTwo.test_one", + "folder.file_two.CaseTwoFileTwo.test_two", + ], + ), + ], +) +def test_simple_test_cases(directory, pattern, expected) -> None: + """The get_test_case fuction should return tests from all test suites.""" + actual = [] + + # Discover tests in .data/<directory>. + start_dir = os.fsdecode(TEST_DATA_PATH / directory) + + loader = unittest.TestLoader() + suite = loader.discover(start_dir, pattern) + + # Iterate on get_test_case and save the test id. + actual = [test.id() for test in get_test_case(suite)] + + assert expected == actual + + +def test_get_existing_child_node() -> None: + """The get_child_node fuction should return the child node of a test tree if it exists.""" + tree: TestNode = { + "name": "root", + "path": "foo", + "type_": TestNodeTypeEnum.folder, + "children": [ + { + "name": "childOne", + "path": "child/one", + "type_": TestNodeTypeEnum.folder, + "children": [ + { + "name": "nestedOne", + "path": "nested/one", + "type_": TestNodeTypeEnum.folder, + "children": [], + "id_": "nested/one", + }, + { + "name": "nestedTwo", + "path": "nested/two", + "type_": TestNodeTypeEnum.folder, + "children": [], + "id_": "nested/two", + }, + ], + "id_": "child/one", + }, + { + "name": "childTwo", + "path": "child/two", + "type_": TestNodeTypeEnum.folder, + "children": [], + "id_": "child/two", + }, + ], + "id_": "foo", + } + + get_child_node("childTwo", "child/two", TestNodeTypeEnum.folder, tree) + tree_copy = tree.copy() + + # Check that the tree didn't get mutated by get_child_node. + assert is_same_tree(tree, tree_copy, ["id_", "lineno", "name"]) + + +def test_no_existing_child_node() -> None: + """The get_child_node fuction should add a child node to a test tree and return it if it does not exist.""" + tree: TestNode = { + "name": "root", + "path": "foo", + "type_": TestNodeTypeEnum.folder, + "children": [ + { + "name": "childOne", + "path": "child/one", + "type_": TestNodeTypeEnum.folder, + "children": [ + { + "name": "nestedOne", + "path": "nested/one", + "type_": TestNodeTypeEnum.folder, + "children": [], + "id_": "nested/one", + }, + { + "name": "nestedTwo", + "path": "nested/two", + "type_": TestNodeTypeEnum.folder, + "children": [], + "id_": "nested/two", + }, + ], + "id_": "child/one", + }, + { + "name": "childTwo", + "path": "child/two", + "type_": TestNodeTypeEnum.folder, + "children": [], + "id_": "child/two", + }, + ], + "id_": "foo", + } + + # Make a separate copy of tree["children"]. + tree_before = tree.copy() + tree_before["children"] = tree["children"][:] + + get_child_node("childThree", "child/three", TestNodeTypeEnum.folder, tree) + + tree_after = tree.copy() + tree_after["children"] = tree_after["children"][:-1] + + # Check that all pre-existing items in the tree didn't get mutated by get_child_node. + assert is_same_tree(tree_before, tree_after, ["id_", "lineno", "name"]) + + # Check for the added node. + last_child = tree["children"][-1] + assert last_child["name"] == "childThree" + + +def test_build_simple_tree() -> None: + """The build_test_tree function should build and return a test tree from discovered test suites, and an empty list of errors if there are none in the discovered data.""" + # Discovery tests in utils_simple_tree.py. + start_dir = os.fsdecode(TEST_DATA_PATH) + pattern = "utils_simple_tree*" + file_path = os.fsdecode(pathlib.PurePath(TEST_DATA_PATH, "utils_simple_tree.py")) + + expected: TestNode = { + "path": start_dir, + "type_": TestNodeTypeEnum.folder, + "name": ".data", + "children": [ + { + "name": "utils_simple_tree.py", + "type_": TestNodeTypeEnum.file, + "path": file_path, + "children": [ + { + "name": "TreeOne", + "path": file_path, + "type_": TestNodeTypeEnum.class_, + "children": [ + { + "name": "test_one", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "13", + "id_": file_path + "\\" + "TreeOne" + "\\" + "test_one", + "runID": "utils_simple_tree.TreeOne.test_one", + }, + { + "name": "test_two", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "16", + "id_": file_path + "\\" + "TreeOne" + "\\" + "test_two", + "runID": "utils_simple_tree.TreeOne.test_two", + }, + ], + "id_": file_path + "\\" + "TreeOne", + } + ], + "id_": file_path, + } + ], + "id_": start_dir, + } + + loader = unittest.TestLoader() + suite = loader.discover(start_dir, pattern) + tests, errors = build_test_tree(suite, start_dir) + + assert is_same_tree(expected, tests, ["id_", "lineno", "name"]) + assert not errors + + +def test_build_decorated_tree() -> None: + """The build_test_tree function should build and return a test tree from discovered test suites, with correct line numbers for decorated test, and an empty list of errors if there are none in the discovered data.""" + # Discovery tests in utils_decorated_tree.py. + start_dir = os.fsdecode(TEST_DATA_PATH) + pattern = "utils_decorated_tree*" + file_path = os.fsdecode(pathlib.PurePath(TEST_DATA_PATH, "utils_decorated_tree.py")) + + expected: TestNode = { + "path": start_dir, + "type_": TestNodeTypeEnum.folder, + "name": ".data", + "children": [ + { + "name": "utils_decorated_tree.py", + "type_": TestNodeTypeEnum.file, + "path": file_path, + "children": [ + { + "name": "TreeOne", + "path": file_path, + "type_": TestNodeTypeEnum.class_, + "children": [ + { + "name": "test_one", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "24", + "id_": file_path + "\\" + "TreeOne" + "\\" + "test_one", + "runID": "utils_decorated_tree.TreeOne.test_one", + }, + { + "name": "test_two", + "path": file_path, + "type_": TestNodeTypeEnum.test, + "lineno": "28", + "id_": file_path + "\\" + "TreeOne" + "\\" + "test_two", + "runID": "utils_decorated_tree.TreeOne.test_two", + }, + ], + "id_": file_path + "\\" + "TreeOne", + } + ], + "id_": file_path, + } + ], + "id_": start_dir, + } + + loader = unittest.TestLoader() + suite = loader.discover(start_dir, pattern) + tests, errors = build_test_tree(suite, start_dir) + + assert is_same_tree(expected, tests, ["id_", "lineno", "name"]) + assert not errors + + +def test_build_empty_tree() -> None: + """The build_test_tree function should return None if there are no discovered test suites, and an empty list of errors if there are none in the discovered data.""" + start_dir = os.fsdecode(TEST_DATA_PATH) + pattern = "does_not_exist*" + + loader = unittest.TestLoader() + suite = loader.discover(start_dir, pattern) + tests, errors = build_test_tree(suite, start_dir) + + assert tests is not None + assert tests.get("children") == [] + assert not errors + + +def test_doctest_standard_blocked() -> None: + """Standard doctests with short IDs should be skipped with an error message.""" + start_dir = os.fsdecode(TEST_DATA_PATH) + pattern = "test_doctest_standard*" + + loader = unittest.TestLoader() + suite = loader.discover(start_dir, pattern) + tests, errors = build_test_tree(suite, start_dir) + + # Should return a tree but with no test children (since doctests are skipped) + assert tests is not None + # Check that we got an error about doctests not being supported + assert len(errors) > 0 + assert "Skipping doctest as it is not supported for the extension" in errors[0] + + +def test_doctest_patched_works() -> None: + """Patched doctests with properly formatted IDs should be processed normally.""" + start_dir = os.fsdecode(TEST_DATA_PATH) + pattern = "test_doctest_patched*" + + loader = unittest.TestLoader() + suite = loader.discover(start_dir, pattern) + tests, errors = build_test_tree(suite, start_dir) + + # Should successfully build a tree with the patched doctest + assert tests is not None + + # The patched doctests should have proper IDs and be included + # We should find at least one test child (the doctests that were patched) + def count_tests(node): + """Recursively count test nodes.""" + if node.get("type_") == "test": + return 1 + count = 0 + for child in node.get("children", []): + count += count_tests(child) + return count + + test_count = count_tests(tests) + # We expect at least the module doctest and function doctest + assert test_count > 0, "Patched doctests should be included in the tree" + # Should not have doctest-related errors since they're properly formatted + assert not any("doctest" in str(e).lower() for e in errors) diff --git a/python_files/tests/util.py b/python_files/tests/util.py new file mode 100644 index 000000000000..ee240cd95202 --- /dev/null +++ b/python_files/tests/util.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + + +class Stub: + def __init__(self): + self.calls = [] + + def add_call(self, name, args=None, kwargs=None): + self.calls.append((name, args, kwargs)) + + +class StubProxy: + 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 = f"{self.name}.{funcname}" + return self.stub.add_call(callname, *args, **kwargs) diff --git a/python_files/unittestadapter/__init__.py b/python_files/unittestadapter/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/python_files/unittestadapter/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. diff --git a/python_files/unittestadapter/discovery.py b/python_files/unittestadapter/discovery.py new file mode 100644 index 000000000000..c864ac76916b --- /dev/null +++ b/python_files/unittestadapter/discovery.py @@ -0,0 +1,156 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys +import traceback +import unittest +from typing import List, Optional + +script_dir = pathlib.Path(__file__).parent +sys.path.append(os.fspath(script_dir)) + +from django_handler import django_discovery_runner # noqa: E402 + +# If I use from utils then there will be an import error in test_discovery.py. +from unittestadapter.pvsc_utils import ( # noqa: E402 + DiscoveryPayloadDict, + VSCodeUnittestError, + build_test_tree, + parse_unittest_args, + send_post_request, +) + + +def discover_tests( + start_dir: str, + pattern: str, + top_level_dir: Optional[str], + project_root_path: Optional[str] = None, +) -> DiscoveryPayloadDict: + """Returns a dictionary containing details of the discovered tests. + + The returned dict has the following keys: + + - cwd: Absolute path to the test start directory (or project_root_path if provided); + - status: Test discovery status, can be "success" or "error"; + - tests: Discoverered tests if any, not present otherwise. Note that the status can be "error" but the payload can still contain tests; + - error: Discovery error if any, not present otherwise. + + Payload format for a successful discovery: + { + "status": "success", + "cwd": <test discovery directory>, + "tests": <test tree> + } + + Payload format for a successful discovery with no tests: + { + "status": "success", + "cwd": <test discovery directory>, + } + + Payload format when there are errors: + { + "cwd": <test discovery directory> + "": [list of errors] + "status": "error", + } + + Args: + start_dir: Directory where test discovery starts + pattern: Pattern to match test files (e.g., "test*.py") + top_level_dir: Top-level directory for the test tree hierarchy + project_root_path: Optional project root path for the cwd in the response payload + (used for project-based testing to root test tree at project) + """ + cwd = os.path.abspath(project_root_path or start_dir) # noqa: PTH100 + if "/" in start_dir: # is a subdir + parent_dir = os.path.dirname(start_dir) # noqa: PTH120 + sys.path.insert(0, parent_dir) + else: + sys.path.insert(0, cwd) + payload: DiscoveryPayloadDict = {"cwd": cwd, "status": "success", "tests": None} + tests = None + error: List[str] = [] + + try: + loader = unittest.TestLoader() + suite = loader.discover(start_dir, pattern, top_level_dir) + + # If the top level directory is not provided, then use the start directory. + if top_level_dir is None: + top_level_dir = start_dir + + # Get abspath of top level directory for build_test_tree. + top_level_dir = os.path.abspath(top_level_dir) # noqa: PTH100 + + tests, error = build_test_tree(suite, top_level_dir) # test tree built successfully here. + + except Exception: + error.append(traceback.format_exc()) + + # Still include the tests in the payload even if there are errors so that the TS + # side can determine if it is from run or discovery. + payload["tests"] = tests if tests is not None else None + + if len(error): + payload["status"] = "error" + payload["error"] = error + + return payload + + +if __name__ == "__main__": + # Get unittest discovery arguments. + argv = sys.argv[1:] + index = argv.index("--udiscovery") + + ( + start_dir, + pattern, + top_level_dir, + _verbosity, + _failfast, + _locals, + ) = parse_unittest_args(argv[index + 1 :]) + + test_run_pipe = os.getenv("TEST_RUN_PIPE") + if not test_run_pipe: + error_msg = ( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of unittest trying to send data. " + "Please confirm this environment variable is not being changed or removed " + "as it is required for successful test discovery and execution." + f"TEST_RUN_PIPE = {test_run_pipe}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) + + if manage_py_path := os.environ.get("MANAGE_PY_PATH"): + # Django configuration requires manage.py path to enable. + print( + f"MANAGE_PY_PATH is set, running Django discovery with path to manage.py as: ${manage_py_path}" + ) + try: + # collect args for Django discovery runner. + args = argv[index + 1 :] or [] + django_discovery_runner(manage_py_path, args) + except Exception as e: + error_msg = f"Error configuring Django test runner: {e}" + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) # noqa: B904 + else: + # Check for PROJECT_ROOT_PATH environment variable (project-based testing). + # When set, this overrides top_level_dir to root the test tree at the project directory. + project_root_path = os.environ.get("PROJECT_ROOT_PATH") + if project_root_path: + top_level_dir = project_root_path + + # Perform regular unittest test discovery. + # Pass project_root_path so the payload's cwd matches the project root. + payload = discover_tests( + start_dir, pattern, top_level_dir, project_root_path=project_root_path + ) + # Post this discovery payload. + send_post_request(payload, test_run_pipe) diff --git a/python_files/unittestadapter/django_handler.py b/python_files/unittestadapter/django_handler.py new file mode 100644 index 000000000000..574aee7af7fa --- /dev/null +++ b/python_files/unittestadapter/django_handler.py @@ -0,0 +1,111 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import subprocess +import sys +from contextlib import contextmanager, suppress +from typing import Generator, List + +script_dir = pathlib.Path(__file__).parent +sys.path.append(os.fspath(script_dir)) +sys.path.insert(0, os.fspath(script_dir / "lib" / "python")) + +from pvsc_utils import ( # noqa: E402 + VSCodeUnittestError, +) + + +@contextmanager +def override_argv(argv: List[str]) -> Generator: + """Context manager to temporarily override sys.argv with the provided arguments.""" + original_argv = sys.argv + sys.argv = argv + try: + yield + finally: + sys.argv = original_argv + + +def django_discovery_runner(manage_py_path: str, args: List[str]) -> None: + # Attempt a small amount of validation on the manage.py path. + if not pathlib.Path(manage_py_path).exists(): + raise VSCodeUnittestError("Error running Django, manage.py path does not exist.") + + try: + # Get path to the custom_test_runner.py parent folder, add to sys.path and new environment used for subprocess. + custom_test_runner_dir = pathlib.Path(__file__).parent + sys.path.insert(0, os.fspath(custom_test_runner_dir)) + env = os.environ.copy() + if "PYTHONPATH" in env: + env["PYTHONPATH"] = os.fspath(custom_test_runner_dir) + os.pathsep + env["PYTHONPATH"] + else: + env["PYTHONPATH"] = os.fspath(custom_test_runner_dir) + + # Build command to run 'python manage.py test'. + command = [ + sys.executable, + manage_py_path, + "test", + "--testrunner=django_test_runner.CustomDiscoveryTestRunner", + ] + command.extend(args) + print("Running Django tests with command:", command) + + subprocess_discovery = subprocess.run( + command, + capture_output=True, + text=True, + env=env, + ) + print(subprocess_discovery.stderr, file=sys.stderr) + print(subprocess_discovery.stdout, file=sys.stdout) + # Zero return code indicates success, 1 indicates test failures, so both are considered successful. + if subprocess_discovery.returncode not in (0, 1): + error_msg = "Django test discovery process exited with non-zero error code See stderr above for more details." + print(error_msg, file=sys.stderr) + except Exception as e: + raise VSCodeUnittestError(f"Error during Django discovery: {e}") # noqa: B904 + + +def django_execution_runner(manage_py_path: str, test_ids: List[str], args: List[str]) -> None: + manage_path: pathlib.Path = pathlib.Path(manage_py_path) + # Attempt a small amount of validation on the manage.py path. + if not manage_path.exists(): + raise VSCodeUnittestError("Error running Django, manage.py path does not exist.") + + try: + # Get path to the custom_test_runner.py parent folder, add to sys.path. + custom_test_runner_dir: pathlib.Path = pathlib.Path(__file__).parent + sys.path.insert(0, os.fspath(custom_test_runner_dir)) + env: dict[str, str] = os.environ.copy() + if "PYTHONPATH" in env: + env["PYTHONPATH"] = os.fspath(custom_test_runner_dir) + os.pathsep + env["PYTHONPATH"] + else: + env["PYTHONPATH"] = os.fspath(custom_test_runner_dir) + + django_project_dir: pathlib.Path = manage_path.parent + sys.path.insert(0, os.fspath(django_project_dir)) + print(f"Django project directory: {django_project_dir}") + + manage_argv: List[str] = [ + str(manage_path), + "test", + "--testrunner=django_test_runner.CustomExecutionTestRunner", + *args, + *test_ids, + ] + print(f"Django manage.py arguments: {manage_argv}") + + try: + argv_context = override_argv(manage_argv) + suppress_context = suppress(SystemExit) + manage_file = manage_path.open() + with argv_context, suppress_context, manage_file: + manage_code = manage_file.read() + exec(manage_code, {"__name__": "__main__", "__file__": manage_path}) + except OSError as e: + raise VSCodeUnittestError("Error running Django, unable to read manage.py") from e + except Exception as e: + print(f"Error during Django test execution: {e}", file=sys.stderr) diff --git a/python_files/unittestadapter/django_test_runner.py b/python_files/unittestadapter/django_test_runner.py new file mode 100644 index 000000000000..c1cca7ac2780 --- /dev/null +++ b/python_files/unittestadapter/django_test_runner.py @@ -0,0 +1,95 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import sys + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) + +from typing import TYPE_CHECKING # noqa: E402 + +from execution import UnittestTestResult # noqa: E402 +from pvsc_utils import ( # noqa: E402 + DiscoveryPayloadDict, + VSCodeUnittestError, + build_test_tree, + send_post_request, +) + +try: + from django.test.runner import DiscoverRunner +except ImportError: + raise ImportError( # noqa: B904 + "Django module not found. Please only use the environment variable MANAGE_PY_PATH if you want to use Django." + ) + + +if TYPE_CHECKING: + import unittest + + +class CustomDiscoveryTestRunner(DiscoverRunner): + """Custom test runner for Django to handle test DISCOVERY and building the test tree.""" + + def run_tests(self, test_labels, **kwargs): + test_run_pipe: str | None = os.getenv("TEST_RUN_PIPE") + if not test_run_pipe: + error_msg = ( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of unittest trying to send data. " + "Please confirm this environment variable is not being changed or removed " + "as it is required for successful test discovery and execution." + f"TEST_RUN_PIPE = {test_run_pipe}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) + try: + top_level_dir: pathlib.Path = pathlib.Path.cwd() + + # Discover tests and build into a tree. + suite: unittest.TestSuite = self.build_suite(test_labels, **kwargs) + tests, error = build_test_tree(suite, os.fspath(top_level_dir)) + + payload: DiscoveryPayloadDict = { + "cwd": os.fspath(top_level_dir), + "status": "success", + "tests": None, + } + payload["tests"] = tests if tests is not None else None + if len(error): + payload["status"] = "error" + payload["error"] = error + + # Send discovery payload. + send_post_request(payload, test_run_pipe) + return 0 # Skip actual test execution, return 0 as no tests were run. + except Exception as e: + error_msg = ( + "DJANGO ERROR: An error occurred while discovering and building the test suite. " + f"Error: {e}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) # noqa: B904 + + +class CustomExecutionTestRunner(DiscoverRunner): + """Custom test runner for Django to handle test EXECUTION and uses UnittestTestResult to send dynamic run results.""" + + def get_test_runner_kwargs(self): + """Override to provide custom test runner; resultclass.""" + test_run_pipe: str | None = os.getenv("TEST_RUN_PIPE") + if not test_run_pipe: + error_msg = ( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of Django trying to send data. " + "Please confirm this environment variable is not being changed or removed " + "as it is required for successful test discovery and execution." + f"TEST_RUN_PIPE = {test_run_pipe}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) + # Get existing kwargs + kwargs = super().get_test_runner_kwargs() + # Add custom resultclass, same resultclass as used in unittest. + kwargs["resultclass"] = UnittestTestResult + return kwargs diff --git a/python_files/unittestadapter/execution.py b/python_files/unittestadapter/execution.py new file mode 100644 index 000000000000..422f246d3476 --- /dev/null +++ b/python_files/unittestadapter/execution.py @@ -0,0 +1,427 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import atexit +import enum +import os +import pathlib +import sys +import sysconfig +import traceback +import unittest +from types import TracebackType +from typing import Dict, List, Optional, Set, Tuple, Type, Union + +# Adds the scripts directory to the PATH as a workaround for enabling shell for test execution. +path_var_name = "PATH" if "PATH" in os.environ else "Path" +os.environ[path_var_name] = ( + sysconfig.get_paths()["scripts"] + os.pathsep + os.environ[path_var_name] +) + +script_dir = pathlib.Path(__file__).parent +sys.path.append(os.fspath(script_dir)) + +from django_handler import django_execution_runner # noqa: E402 + +from unittestadapter.pvsc_utils import ( # noqa: E402 + CoveragePayloadDict, + ExecutionPayloadDict, + FileCoverageInfo, + TestExecutionStatus, + VSCodeUnittestError, + parse_unittest_args, + send_post_request, +) + +ErrorType = Union[Tuple[Type[BaseException], BaseException, TracebackType], Tuple[None, None, None]] +test_run_pipe = "" +START_DIR = "" +# PROJECT_ROOT_PATH: Used for project-based testing to override cwd in payload +# When set, this should be used as the cwd in all execution payloads +PROJECT_ROOT_PATH = None # type: Optional[str] + + +class TestOutcomeEnum(str, enum.Enum): + error = "error" + failure = "failure" + success = "success" + skipped = "skipped" + expected_failure = "expected-failure" + unexpected_success = "unexpected-success" + subtest_success = "subtest-success" + subtest_failure = "subtest-failure" + + +class UnittestTestResult(unittest.TextTestResult): + def __init__(self, *args, **kwargs): + self.formatted: Dict[str, Dict[str, Union[str, None]]] = {} + super().__init__(*args, **kwargs) + + def startTest(self, test: unittest.TestCase): # noqa: N802 + super().startTest(test) + + def stopTestRun(self): # noqa: N802 + super().stopTestRun() + + def addError( # noqa: N802 + self, + test: unittest.TestCase, + err: ErrorType, + ): + super().addError(test, err) + self.formatResult(test, TestOutcomeEnum.error, err) + + def addFailure( # noqa: N802 + self, + test: unittest.TestCase, + err: ErrorType, + ): + super().addFailure(test, err) + self.formatResult(test, TestOutcomeEnum.failure, err) + + def addSuccess(self, test: unittest.TestCase): # noqa: N802 + super().addSuccess(test) + self.formatResult(test, TestOutcomeEnum.success) + + def addSkip(self, test: unittest.TestCase, reason: str): # noqa: N802 + super().addSkip(test, reason) + self.formatResult(test, TestOutcomeEnum.skipped) + + def addExpectedFailure(self, test: unittest.TestCase, err: ErrorType): # noqa: N802 + super().addExpectedFailure(test, err) + self.formatResult(test, TestOutcomeEnum.expected_failure, err) + + def addUnexpectedSuccess(self, test: unittest.TestCase): # noqa: N802 + super().addUnexpectedSuccess(test) + self.formatResult(test, TestOutcomeEnum.unexpected_success) + + def addSubTest( # noqa: N802 + self, + test: unittest.TestCase, + subtest: unittest.TestCase, + err: Union[ErrorType, None], + ): + super().addSubTest(test, subtest, err) + self.formatResult( + test, + TestOutcomeEnum.subtest_failure if err else TestOutcomeEnum.subtest_success, + err, + subtest, + ) + + def formatResult( # noqa: N802 + self, + test: unittest.TestCase, + outcome: str, + error: Union[ErrorType, None] = None, + subtest: Union[unittest.TestCase, None] = None, + ): + tb = None + + message = "" + # error is a tuple of the form returned by sys.exc_info(): (type, value, traceback). + if error is not None: + try: + message = f"{error[0]} {error[1]}" + except Exception: + message = "Error occurred, unknown type or value" + formatted = traceback.format_exception(*error) + tb = "".join(formatted) + # Remove the 'Traceback (most recent call last)' + formatted = formatted[1:] + test_id = subtest.id() if subtest else test.id() + + result = { + "test": test.id(), + "outcome": outcome, + "message": message, + "traceback": tb, + "subtest": subtest.id() if subtest else None, + } + self.formatted[test_id] = result + test_run_pipe = os.getenv("TEST_RUN_PIPE") + if not test_run_pipe: + print( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of unittest trying to send data. " + f"TEST_RUN_PIPE = {test_run_pipe}\n", + file=sys.stderr, + ) + raise VSCodeUnittestError( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of unittest trying to send data. " + ) + send_run_data(result, test_run_pipe) + + +def filter_tests(suite: unittest.TestSuite, test_ids: List[str]) -> unittest.TestSuite: + """Filter the tests in the suite to only run the ones with the given ids.""" + filtered_suite = unittest.TestSuite() + for test in suite: + if isinstance(test, unittest.TestCase): + if test.id() in test_ids: + filtered_suite.addTest(test) + else: + filtered_suite.addTest(filter_tests(test, test_ids)) + return filtered_suite + + +def get_all_test_ids(suite: unittest.TestSuite) -> List[str]: + """Return a list of all test ids in the suite.""" + test_ids = [] + for test in suite: + if isinstance(test, unittest.TestCase): + test_ids.append(test.id()) + else: + test_ids.extend(get_all_test_ids(test)) + return test_ids + + +def find_missing_tests(test_ids: List[str], suite: unittest.TestSuite) -> List[str]: + """Return a list of test ids that are not in the suite.""" + all_test_ids = get_all_test_ids(suite) + return [test_id for test_id in test_ids if test_id not in all_test_ids] + + +# Args: start_path path to a directory or a file, list of ids that may be empty. +# Edge cases: +# - if tests got deleted since the VS Code side last ran discovery and the current test run, +# return these test ids in the "not_found" entry, and the VS Code side can process them as "unknown"; +# - if tests got added since the VS Code side last ran discovery and the current test run, ignore them. +def run_tests( + start_dir: str, + test_ids: List[str], + pattern: str, + top_level_dir: Optional[str], + verbosity: int, + failfast: Optional[bool], # noqa: FBT001 + locals_: Optional[bool] = None, # noqa: FBT001 + project_root_path: Optional[str] = None, +) -> ExecutionPayloadDict: + """Run unittests and return the execution payload. + + Args: + start_dir: Directory where test discovery starts + test_ids: List of test IDs to run + pattern: Pattern to match test files + top_level_dir: Top-level directory for test tree hierarchy + verbosity: Verbosity level for test output + failfast: Stop on first failure + locals_: Show local variables in tracebacks + project_root_path: Optional project root path for the cwd in the response payload + (used for project-based testing to root test tree at project) + """ + cwd = os.path.abspath(project_root_path or start_dir) # noqa: PTH100 + if "/" in start_dir: # is a subdir + parent_dir = os.path.dirname(start_dir) # noqa: PTH120 + sys.path.insert(0, parent_dir) + else: + sys.path.insert(0, cwd) + status = TestExecutionStatus.error + error = None + payload: ExecutionPayloadDict = {"cwd": cwd, "status": status, "result": None} + + try: + # If it's a file, split path and file name. + start_dir = cwd + if cwd.endswith(".py"): + start_dir = os.path.dirname(cwd) # noqa: PTH120 + pattern = os.path.basename(cwd) # noqa: PTH119 + + if failfast is None: + failfast = False + if locals_ is None: + locals_ = False + if verbosity is None: + verbosity = 1 + runner = unittest.TextTestRunner( + resultclass=UnittestTestResult, + tb_locals=locals_, + failfast=failfast, + verbosity=verbosity, + ) + + # Discover tests at path with the file name as a pattern (if any). + loader = unittest.TestLoader() + suite = loader.discover(start_dir, pattern, top_level_dir) + + # lets try to tailer our own suite so we can figure out running only the ones we want + tailor: unittest.TestSuite = filter_tests(suite, test_ids) + + # If any tests are missing, add them to the payload. + not_found = find_missing_tests(test_ids, tailor) + if not_found: + missing_suite = loader.loadTestsFromNames(not_found) + tailor.addTests(missing_suite) + + result: UnittestTestResult = runner.run(tailor) # type: ignore + + payload["result"] = result.formatted + + except Exception: + status = TestExecutionStatus.error + error = traceback.format_exc() + + if error is not None: + payload["error"] = error + else: + status = TestExecutionStatus.success + + payload["status"] = status + + return payload + + +__socket = None +atexit.register(lambda: __socket.close() if __socket else None) + + +def send_run_data(raw_data, test_run_pipe): + status = raw_data["outcome"] + # Use PROJECT_ROOT_PATH if set (project-based testing), otherwise use START_DIR + cwd = os.path.abspath(PROJECT_ROOT_PATH or START_DIR) # noqa: PTH100 + test_id = raw_data["subtest"] or raw_data["test"] + test_dict = {} + test_dict[test_id] = raw_data + payload: ExecutionPayloadDict = {"cwd": cwd, "status": status, "result": test_dict} + send_post_request(payload, test_run_pipe) + + +if __name__ == "__main__": + # Get unittest test execution arguments. + argv = sys.argv[1:] + index = argv.index("--udiscovery") + + ( + start_dir, + pattern, + top_level_dir, + verbosity, + failfast, + locals_, + ) = parse_unittest_args(argv[index + 1 :]) + + run_test_ids_pipe = os.environ.get("RUN_TEST_IDS_PIPE") + test_run_pipe = os.getenv("TEST_RUN_PIPE") + if not run_test_ids_pipe: + print("Error[vscode-unittest]: RUN_TEST_IDS_PIPE env var is not set.", file=sys.stderr) + raise VSCodeUnittestError("Error[vscode-unittest]: RUN_TEST_IDS_PIPE env var is not set.") + if not test_run_pipe: + print("Error[vscode-unittest]: TEST_RUN_PIPE env var is not set.", file=sys.stderr) + raise VSCodeUnittestError("Error[vscode-unittest]: TEST_RUN_PIPE env var is not set.") + test_ids = [] + cwd = pathlib.Path(start_dir).absolute() + try: + # Read the test ids from the file, attempt to delete file afterwords. + ids_path = pathlib.Path(run_test_ids_pipe) + test_ids = ids_path.read_text(encoding="utf-8").splitlines() + try: + ids_path.unlink() + except Exception as e: + print(f"Error[vscode-unittest]: unable to delete temp file: {e}", file=sys.stderr) + + except Exception as e: + # No test ids received from buffer, return error payload + status: TestExecutionStatus = TestExecutionStatus.error + payload: ExecutionPayloadDict = { + "cwd": str(cwd), + "status": status, + "result": None, + "error": "No test ids read from temp file," + str(e), + } + send_post_request(payload, test_run_pipe) + + workspace_root = os.environ.get("COVERAGE_ENABLED") + # For unittest COVERAGE_ENABLED is to the root of the workspace so correct data is collected + cov = None + is_coverage_run = os.environ.get("COVERAGE_ENABLED") is not None + include_branches = False + if is_coverage_run: + import coverage + + # insert "python_files/lib/python" into the path so packaging can be imported + python_files_dir = pathlib.Path(__file__).parent.parent + bundled_dir = pathlib.Path(python_files_dir / "lib" / "python") + sys.path.append(os.fspath(bundled_dir)) + + from packaging.version import Version + + coverage_version = Version(coverage.__version__) + # only include branches if coverage version is 7.7.0 or greater (as this was when the api saves) + if coverage_version >= Version("7.7.0"): + include_branches = True + + source_ar: List[str] = [] + if workspace_root: + source_ar.append(workspace_root) + if top_level_dir: + source_ar.append(top_level_dir) + if start_dir: + source_ar.append(os.path.abspath(start_dir)) # noqa: PTH100 + cov = coverage.Coverage( + branch=include_branches, source=source_ar + ) # is at least 1 of these required?? + cov.start() + + # If no error occurred, we will have test ids to run. + if manage_py_path := os.environ.get("MANAGE_PY_PATH"): + args = argv[index + 1 :] or [] + django_execution_runner(manage_py_path, test_ids, args) + else: + # Check for PROJECT_ROOT_PATH environment variable (project-based testing). + # When set, this overrides the cwd in the payload to match the project root. + project_root_path = os.environ.get("PROJECT_ROOT_PATH") + if project_root_path: + # Update the module-level variable for send_run_data to use + # pylint: disable=global-statement + globals()["PROJECT_ROOT_PATH"] = project_root_path + + # Perform regular unittest execution. + # Pass project_root_path so the payload's cwd matches the project root. + payload = run_tests( + start_dir, + test_ids, + pattern, + top_level_dir, + verbosity, + failfast, + locals_, + project_root_path=project_root_path, + ) + + if is_coverage_run: + import coverage + + if not cov: + raise VSCodeUnittestError("Coverage is enabled but cov is not set") + cov.stop() + cov.save() + cov.load() + file_set: Set[str] = cov.get_data().measured_files() + file_coverage_map: Dict[str, FileCoverageInfo] = {} + for file in file_set: + analysis = cov.analysis2(file) + taken_file_branches = 0 + total_file_branches = -1 + + if include_branches: + branch_stats: dict[int, tuple[int, int]] = cov.branch_stats(file) + total_file_branches = sum([total_exits for total_exits, _ in branch_stats.values()]) + taken_file_branches = sum([taken_exits for _, taken_exits in branch_stats.values()]) + + lines_executable = {int(line_no) for line_no in analysis[1]} + lines_missed = {int(line_no) for line_no in analysis[3]} + lines_covered = lines_executable - lines_missed + file_info: FileCoverageInfo = { + "lines_covered": list(lines_covered), # list of int + "lines_missed": list(lines_missed), # list of int + "executed_branches": taken_file_branches, + "total_branches": total_file_branches, + } + file_coverage_map[file] = file_info + + payload_cov: CoveragePayloadDict = CoveragePayloadDict( + coverage=True, + cwd=os.fspath(cwd), + result=file_coverage_map, + error=None, + ) + send_post_request(payload_cov, test_run_pipe) diff --git a/python_files/unittestadapter/pvsc_utils.py b/python_files/unittestadapter/pvsc_utils.py new file mode 100644 index 000000000000..d6920592a4d4 --- /dev/null +++ b/python_files/unittestadapter/pvsc_utils.py @@ -0,0 +1,390 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import atexit +import doctest +import enum +import inspect +import json +import os +import pathlib +import sys +import unittest +from typing import Dict, List, Literal, Optional, Tuple, TypedDict, Union + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) + +from typing_extensions import NotRequired # noqa: E402 + +# Types + + +# Inherit from str so it's JSON serializable. +class TestNodeTypeEnum(str, enum.Enum): + class_ = "class" + file = "file" + folder = "folder" + test = "test" + + +class TestData(TypedDict): + name: str + path: str + type_: TestNodeTypeEnum + id_: str + + +class TestItem(TestData): + lineno: str + runID: str + + +class TestNode(TestData): + children: "List[TestNode | TestItem]" + lineno: NotRequired[str] # Optional field for class nodes + + +class TestExecutionStatus(str, enum.Enum): + error = "error" + success = "success" + + +class VSCodeUnittestError(Exception): + """A custom exception class for unittest errors.""" + + def __init__(self, message): + super().__init__(message) + + +class DiscoveryPayloadDict(TypedDict): + cwd: str + status: Literal["success", "error"] + tests: Optional[TestNode] + error: NotRequired[List[str]] + + +class ExecutionPayloadDict(TypedDict): + cwd: str + status: TestExecutionStatus + result: Optional[Dict[str, Dict[str, Optional[str]]]] + not_found: NotRequired[List[str]] + error: NotRequired[str] + + +class FileCoverageInfo(TypedDict): + lines_covered: List[int] + lines_missed: List[int] + executed_branches: int + total_branches: int + + +class CoveragePayloadDict(Dict): + """A dictionary that is used to send a execution post request to the server.""" + + coverage: bool + cwd: str + result: Optional[Dict[str, FileCoverageInfo]] + error: Optional[str] # Currently unused need to check + + +# Helper functions for data retrieval. + + +def get_test_case(suite): + """Iterate through a unittest test suite and return all test cases.""" + for test in suite: + if isinstance(test, unittest.TestCase): + yield test + else: + yield from get_test_case(test) + + +def get_class_line(test_case: unittest.TestCase) -> Optional[str]: + """Get the line number where a test class is defined.""" + try: + test_class = test_case.__class__ + _sourcelines, lineno = inspect.getsourcelines(test_class) + return str(lineno) + except Exception: + return None + + +def get_source_line(obj) -> str: + """Get the line number of a test case start line.""" + try: + sourcelines, lineno = inspect.getsourcelines(obj) + except Exception: + try: + # tornado-specific, see https://github.com/microsoft/vscode-python/issues/17285. + sourcelines, lineno = inspect.getsourcelines(obj.orig_method) + except Exception: + return "*" + + # Return the line number of the first line of the test case definition. + for i, v in enumerate(sourcelines): + if v.strip().startswith(("def", "async def")): + return str(lineno + i) + + return "*" + + +# Helper functions for test tree building. + + +def build_test_node(path: str, name: str, type_: TestNodeTypeEnum) -> TestNode: + """Build a test node with no children. A test node can be a folder, a file or a class.""" + ## figure out if we are folder, file, or class + id_gen = path + if type_ == TestNodeTypeEnum.folder or type_ == TestNodeTypeEnum.file: + id_gen = path + else: + # means we have to build test node for class + id_gen = path + "\\" + name + + return {"path": path, "name": name, "type_": type_, "children": [], "id_": id_gen} + + +def get_child_node(name: str, path: str, type_: TestNodeTypeEnum, root: TestNode) -> TestNode: + """Find a child node in a test tree given its name, type and path. + + If the node doesn't exist, create it. + Path is required to distinguish between nodes with the same name and type. + """ + try: + result = next( + node + for node in root["children"] + if node["name"] == name and node["type_"] == type_ and node["path"] == path + ) + except StopIteration: + result = build_test_node(path, name, type_) + root["children"].append(result) + + return result # type:ignore + + +def build_test_tree( + suite: unittest.TestSuite, top_level_directory: str +) -> Tuple[Union[TestNode, None], List[str]]: + """Build a test tree from a unittest test suite. + + This function returns the test tree, and any errors found by unittest. + If no tests were discovered, return `None` and a list of errors (if any). + + Test tree structure: + { + "path": <test_directory path>, + "type": "folder", + "name": <folder name>, + "children": [ + { files and folders } + ... + { + "path": <file path>, + "name": filename.py, + "type_": "file", + "children": [ + { + "path": <class path>, + "name": <class name>, + "type_": "class", + "children": [ + { + "path": <test path>, + "name": <test name>, + "type_": "test", + "lineno": <line number> + "id_": <test case id following format in line 196>, + } + ], + "id_": <class path path following format after path> + } + ], + "id_": <file path> + } + ], + "id_": <test_directory path> + } + """ + error = [] + directory_path = pathlib.PurePath(top_level_directory) + root = build_test_node(top_level_directory, directory_path.name, TestNodeTypeEnum.folder) + + for test_case in get_test_case(suite): + test_id = test_case.id() + if test_id.startswith("unittest.loader._FailedTest"): + error.append(str(test_case._exception)) # type: ignore # noqa: SLF001 + elif test_id.startswith("unittest.loader.ModuleSkipped"): + components = test_id.split(".") + class_name = f"{components[-1]}.py" + # Find/build class node. + file_path = os.fsdecode(directory_path / class_name) + current_node = get_child_node(class_name, file_path, TestNodeTypeEnum.file, root) + else: + # Get the static test path components: filename, class name and function name. + components = test_id.split(".") + # Check if this is a doctest with insufficient components that would cause unpacking to fail + if len(components) < 3 and isinstance(test_case, doctest.DocTestCase): + print( + "Skipping doctest as it is not supported for the extension. Test case: ", + test_case, + ) + error = ["Skipping doctest as it is not supported for the extension."] + continue + *folders, filename, class_name, function_name = components + py_filename = f"{filename}.py" + + current_node = root + + # Find/build nodes for the intermediate folders in the test path. + for folder in folders: + current_node = get_child_node( + folder, + os.fsdecode(pathlib.PurePath(current_node["path"], folder)), + TestNodeTypeEnum.folder, + current_node, + ) + + # Find/build file node. + path_components = [top_level_directory, *folders, py_filename] + file_path = os.fsdecode(pathlib.PurePath("/".join(path_components))) + current_node = get_child_node( + py_filename, file_path, TestNodeTypeEnum.file, current_node + ) + + # Find/build class node. + current_node = get_child_node( + class_name, file_path, TestNodeTypeEnum.class_, current_node + ) + + # Add line number to class node if not already present. + if "lineno" not in current_node: + class_lineno = get_class_line(test_case) + if class_lineno is not None: + current_node["lineno"] = class_lineno + + # Get test line number. + test_method = getattr(test_case, test_case._testMethodName) # noqa: SLF001 + lineno = get_source_line(test_method) + + # Add test node. + test_node: TestItem = { + "name": function_name, + "path": file_path, + "lineno": lineno, + "type_": TestNodeTypeEnum.test, + "id_": file_path + "\\" + class_name + "\\" + function_name, + "runID": test_id, + } # concatenate class name and function test name + current_node["children"].append(test_node) + + return root, error + + +def parse_unittest_args( + args: List[str], +) -> Tuple[str, str, Union[str, None], int, Union[bool, None], Union[bool, None]]: + """Parse command-line arguments that should be forwarded to unittest to perform discovery. + + Valid unittest arguments are: -v, -s, -p, -t and their long-form counterparts, + however we only care about the last three. + + The returned tuple contains the following items + - start_directory: The directory where to start discovery, defaults to . + - pattern: The pattern to match test files, defaults to test*.py + - top_level_directory: The top-level directory of the project, defaults to None, + and unittest will use start_directory behind the scenes. + """ + arg_parser = argparse.ArgumentParser() + arg_parser.add_argument("--start-directory", "-s", default=".") + arg_parser.add_argument("--pattern", "-p", default="test*.py") + arg_parser.add_argument("--top-level-directory", "-t", default=None) + arg_parser.add_argument("--failfast", "-f", action="store_true", default=None) + arg_parser.add_argument("--verbose", "-v", action="store_true", default=None) + arg_parser.add_argument("-q", "--quiet", action="store_true", default=None) + arg_parser.add_argument("--locals", action="store_true", default=None) + + parsed_args, _ = arg_parser.parse_known_args(args) + + verbosity: int = 1 + if parsed_args.quiet: + verbosity = 0 + elif parsed_args.verbose: + verbosity = 2 + + return ( + parsed_args.start_directory, + parsed_args.pattern, + parsed_args.top_level_directory, + verbosity, + parsed_args.failfast, + parsed_args.locals, + ) + + +__writer = None +atexit.register(lambda: __writer.close() if __writer else None) + + +def send_post_request( + payload: Union[ExecutionPayloadDict, DiscoveryPayloadDict, CoveragePayloadDict], + test_run_pipe: Optional[str], +): + """ + Sends a post request to the server. + + Keyword arguments: + payload -- the payload data to be sent. + test_run_pipe -- the name of the pipe to send the data to. + """ + if not test_run_pipe: + error_msg = ( + "UNITTEST ERROR: TEST_RUN_PIPE is not set at the time of unittest trying to send data. " + "Please confirm this environment variable is not being changed or removed " + "as it is required for successful test discovery and execution." + f"TEST_RUN_PIPE = {test_run_pipe}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodeUnittestError(error_msg) + + global __writer + + if __writer is None: + try: + __writer = open(test_run_pipe, "wb") # noqa: SIM115, PTH123 + except Exception as error: + error_msg = f"Error attempting to connect to extension named pipe {test_run_pipe}[vscode-unittest]: {error}" + print(error_msg, file=sys.stderr) + __writer = None + raise VSCodeUnittestError(error_msg) from error + + rpc = { + "jsonrpc": "2.0", + "params": payload, + } + data = json.dumps(rpc) + try: + if __writer: + request = ( + f"""content-length: {len(data)}\r\ncontent-type: application/json\r\n\r\n{data}""" + ) + size = 4096 + encoded = request.encode("utf-8") + bytes_written = 0 + while bytes_written < len(encoded): + segment = encoded[bytes_written : bytes_written + size] + bytes_written += __writer.write(segment) + __writer.flush() + else: + print( + f"Connection error[vscode-unittest], writer is None \n[vscode-unittest] data: \n{data} \n", + file=sys.stderr, + ) + except Exception as error: + print( + f"Exception thrown while attempting to send data[vscode-unittest]: {error} \n[vscode-unittest] data: \n{data}\n", + file=sys.stderr, + ) diff --git a/python_files/visualstudio_py_testlauncher.py b/python_files/visualstudio_py_testlauncher.py new file mode 100644 index 000000000000..878491083a71 --- /dev/null +++ b/python_files/visualstudio_py_testlauncher.py @@ -0,0 +1,382 @@ +# 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" + +import contextlib +import json +import os +import signal +import socket +import sys +import traceback +import unittest + +try: + import thread +except ModuleNotFoundError: + import _thread as thread + + +class _TestOutput: + """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_info[0] >= 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 "<stdout>" + else: + return "<stderr>" + + def __getattr__(self, name): + return getattr(self.old_out, name) + + +class _TestOutputBuffer: + 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: + 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.read_socket, ()) + + def close(self): + self._closed = True + + def read_socket(self): + try: + 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 = f"Content-Length: {len(content)}\n\n".encode() + self.socket.send(headers) + self.socket.send(content) + + +_channel = None + + +class VsTestResult(unittest.TextTestResult): + def startTest(self, test): # noqa: N802 + super().startTest(test) + if _channel is not None: + _channel.send_event(name="start", test=test.id()) + + def addError(self, test, err): # noqa: N802 + super().addError(test, err) + self.sendResult(test, "error", err) + + def addFailure(self, test, err): # noqa: N802 + super().addFailure(test, err) + self.sendResult(test, "failed", err) + + def addSuccess(self, test): # noqa: N802 + super().addSuccess(test) + self.sendResult(test, "passed") + + def addSkip(self, test, reason): # noqa: N802 + super().addSkip(test, reason) + self.sendResult(test, "skipped") + + def addExpectedFailure(self, test, err): # noqa: N802 + super().addExpectedFailure(test, err) + self.sendResult(test, "failed-expected", err) + + def addUnexpectedSuccess(self, test): # noqa: N802 + super().addUnexpectedSuccess(test) + self.sendResult(test, "passed-unexpected") + + def addSubTest(self, test, subtest, err): # noqa: N802 + super().addSubTest(test, subtest, err) + self.sendResult(test, "subtest-passed" if err is None else "subtest-failed", err, subtest) + + def sendResult(self, test, outcome, trace=None, subtest=None): # noqa: N802 + 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]) + + result = { + "outcome": outcome, + "traceback": tb, + "message": message, + "test": test.id(), + } + if subtest is not None: + result["subtest"] = subtest.id() + _channel.send_event("result", **result) + + +def stop_tests(): + try: + os.kill(os.getpid(), signal.SIGUSR1) + except Exception: + os.kill(os.getpid(), signal.SIGTERM) + + +class ExitCommand(Exception): # noqa: N818 + pass + + +def signal_handler(signal, frame): # noqa: ARG001 + 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 [<option>] <test names>... ", + ) + parser.add_option("--debug", action="store_true", help="Whether debugging the unit tests") + parser.add_option( + "-x", + "--mixed-mode", + action="store_true", + help="wait for mixed-mode debugger to attach", + ) + parser.add_option( + "-t", + "--test", + type="str", + dest="tests", + action="append", + help="specifies a test to run", + ) + parser.add_option("--testFile", type="str", help="Fully qualitified path to file name") + parser.add_option( + "-c", "--coverage", type="str", help="enable code coverage and specify filename" + ) + parser.add_option( + "-r", + "--result-port", + type="int", + help="connect to port on localhost and send test results", + ) + parser.add_option("--us", type="str", help="Directory to start discovery") + parser.add_option("--up", type="str", help="Pattern to match test files (test*.py default)") + parser.add_option( + "--ut", + type="str", + help="Top level directory of project (default to start directory)", + ) + parser.add_option( + "--uvInt", + "--verboseInt", + type="int", + help="Verbose output (0 none, 1 (no -v) simple, 2 (-v) full)", + ) + parser.add_option("--uf", "--failfast", type="str", help="Stop on first failure") + parser.add_option("--uc", "--catch", type="str", help="Catch control-C and display results") + (opts, _) = parser.parse_args() + + sys.path[0] = os.getcwd() # noqa: PTH109 + if opts.result_port: + try: + signal.signal(signal.SIGUSR1, signal_handler) + except Exception: + with contextlib.suppress(Exception): + signal.signal(signal.SIGTERM, signal_handler) + _channel = _IpcChannel( + socket.create_connection(("127.0.0.1", opts.result_port)), stop_tests + ) + sys.stdout = _TestOutput(sys.stdout, is_stdout=True) + sys.stderr = _TestOutput(sys.stderr, is_stdout=False) + + if opts.mixed_mode: + # For mixed-mode attach, there's no ptvsd and hence no wait_for_attach(), + # so we have to use Win32 API in a loop to do the same thing. + from ctypes import c_char, windll + from time import sleep + + while True: + if windll.kernel32.IsDebuggerPresent() != 0: + break + sleep(0.1) + try: + debugger_helper = windll["Microsoft.PythonTools.Debugger.Helper.x86.dll"] + except OSError: + debugger_helper = windll["Microsoft.PythonTools.Debugger.Helper.x64.dll"] + is_tracing = c_char.in_dll(debugger_helper, "isTracing") + while True: + if is_tracing.value != 0: + break + sleep(0.1) + + cov = None + try: + if opts.coverage: + with contextlib.suppress(Exception): + import coverage + + cov = coverage.coverage(opts.coverage) + cov.load() + cov.start() + if opts.tests is None and opts.testFile is None: + if opts.us is None: + opts.us = "." + if opts.up is None: + opts.up = "test*.py" + tests = unittest.defaultTestLoader.discover(opts.us, opts.up) + else: + # loadTestsFromNames doesn't work well (with duplicate file names or class names) + # Easier approach is find the test suite and use that for running + loader = unittest.TestLoader() + # opts.us will be passed in + suites = loader.discover( + opts.us, + pattern=os.path.basename(opts.testFile), # noqa: PTH119 + top_level_dir=opts.ut, + ) + suite = None + tests = None + if opts.tests is None: + # Run everything in the test file + tests = suites + else: + # Run a specific test class or test method + for test_suite in suites._tests: # noqa: SLF001 + for cls in test_suite._tests: # noqa: SLF001 + with contextlib.suppress(Exception): + for m in cls._tests: + test_id = m.id() + if test_id.startswith(opts.tests[0]): + suite = cls + if test_id in opts.tests: + if tests is None: + tests = unittest.TestSuite([m]) + else: + tests.addTest(m) + if tests is None: + tests = suite + if tests is None and suite is None: + _channel.send_event( + name="error", + outcome="", + traceback="", + message="Failed to identify the test", + test="", + ) + if opts.uvInt is None: + opts.uvInt = 0 + if opts.uf is not None: + runner = unittest.TextTestRunner( + verbosity=opts.uvInt, resultclass=VsTestResult, failfast=True + ) + else: + runner = unittest.TextTestRunner(verbosity=opts.uvInt, resultclass=VsTestResult) + result = runner.run(tests) + if _channel is not None: + _channel.close() + sys.exit(not result.wasSuccessful()) + finally: + if cov is not None: + cov.stop() + cov.save() + cov.xml_report(outfile=opts.coverage + ".xml", omit=__file__) + if _channel is not None: + _channel.send_event(name="done") + _channel.socket.close() + # prevent generation of the error 'Error in sys.exitfunc:' + with contextlib.suppress(Exception): + sys.stdout.close() + with contextlib.suppress(Exception): + sys.stderr.close() + + +if __name__ == "__main__": + main() diff --git a/src/test/pythonFiles/autoimport/two/three.py b/python_files/vscode_datascience_helpers/__init__.py similarity index 100% rename from src/test/pythonFiles/autoimport/two/three.py rename to python_files/vscode_datascience_helpers/__init__.py diff --git a/src/test/pythonFiles/docstrings/one.py b/python_files/vscode_datascience_helpers/tests/__init__.py similarity index 100% rename from src/test/pythonFiles/docstrings/one.py rename to python_files/vscode_datascience_helpers/tests/__init__.py diff --git a/python_files/vscode_datascience_helpers/tests/logParser.py b/python_files/vscode_datascience_helpers/tests/logParser.py new file mode 100644 index 000000000000..12c090ec581f --- /dev/null +++ b/python_files/vscode_datascience_helpers/tests/logParser.py @@ -0,0 +1,95 @@ +import argparse # noqa: N999 +import os +import re +from io import TextIOWrapper +from pathlib import Path + +os.system("color") + + +parser = argparse.ArgumentParser(description="Parse a test log into its parts") +parser.add_argument("testlog", type=str, nargs=1, help="Log to parse") +parser.add_argument("--testoutput", action="store_true", help="Show all failures and passes") +parser.add_argument( + "--split", + action="store_true", + help="Split into per process files. Each file will have the pid appended", +) +ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") +pid_regex = re.compile(r"(\d+).*") +timestamp_regex = re.compile(r"\d{4}-\d{2}-\d{2}T.*\dZ") + + +def strip_timestamp(line: str): + match = timestamp_regex.match(line) + if match: + return line[match.end() :] + return line + + +def read_strip_lines(f: TextIOWrapper): + return map(strip_timestamp, f.readlines()) + + +def print_test_output(testlog): + # Find all the lines that don't have a PID in them. These are the test output + p = Path(testlog[0]) + with p.open() as f: + for line in read_strip_lines(f): + stripped = line.strip() + if len(stripped) > 2 and stripped[0] == "\x1b" and stripped[1] == "[": + print(line.rstrip()) # Should be a test line as it has color encoding + + +def split_by_pid(testlog): + # Split testlog into prefixed logs based on pid + p = Path(testlog[0]) + pids = set() + logs = {} + pid = None + try: + with p.open() as f: + for line in read_strip_lines(f): + stripped = ansi_escape.sub("", line.strip()) + if len(stripped) > 0: + # Pull out the pid + match = pid_regex.match(stripped) + + # Pids are at least two digits + if match and len(match.group(1)) > 2: + # Pid is found + pid = int(match.group(1)) + + # See if we've created a log for this pid or not + if pid not in pids: + pids.add(pid) + log_file = p.with_name(f"{p.stem}_{pid}.log") + print("Writing to new log:", os.fsdecode(log_file)) + logs[pid] = log_file.open(mode="w") + + # Add this line to the log + if pid is not None: + logs[pid].write(line) + finally: + # Close all of the open logs + for key in logs: + logs[key].close() + + +def do_work(args): + if not args.testlog: + print("Test log should be passed") + elif args.testoutput: + print_test_output(args.testlog) + elif args.split: + split_by_pid(args.testlog) + else: + parser.print_usage() + + +def main(): + do_work(parser.parse_args()) + + +if __name__ == "__main__": + main() diff --git a/python_files/vscode_pytest/__init__.py b/python_files/vscode_pytest/__init__.py new file mode 100644 index 000000000000..be4e3daaa843 --- /dev/null +++ b/python_files/vscode_pytest/__init__.py @@ -0,0 +1,1197 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from __future__ import annotations + +import atexit +import contextlib +import json +import os +import pathlib +import sys +import traceback +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generator, + Literal, + Protocol, + TypedDict, + cast, +) + +import pytest + +if TYPE_CHECKING: + from pluggy import Result + from typing_extensions import NotRequired + +USES_PYTEST_DESCRIBE = False + +with contextlib.suppress(ImportError): + from pytest_describe.plugin import DescribeBlock + + USES_PYTEST_DESCRIBE = True + + +class HasPathOrFspath(Protocol): + """Protocol defining objects that have either a path or fspath attribute.""" + + path: pathlib.Path | None = None + fspath: Any | None = None + + +class TestData(TypedDict): + """A general class that all test objects inherit from.""" + + name: str + path: pathlib.Path + type_: Literal["class", "function", "file", "folder", "test", "error"] + id_: str + + +class TestItem(TestData): + """A class defining test items.""" + + lineno: str + runID: str + + +class TestNode(TestData): + """A general class that handles all test data which contains children.""" + + children: list[TestNode | TestItem | None] + lineno: NotRequired[str] # Optional field for class/function nodes + + +class VSCodePytestError(Exception): + """A custom exception class for pytest errors.""" + + def __init__(self, message): + super().__init__(message) + + +ERRORS = [] +IS_DISCOVERY = False +map_id_to_path = {} +collected_tests_so_far = set() +TEST_RUN_PIPE = os.getenv("TEST_RUN_PIPE") +PROJECT_ROOT_PATH = os.getenv( + "PROJECT_ROOT_PATH" +) # Path to project root for multi-project workspaces +SYMLINK_PATH = None +INCLUDE_BRANCHES = False + +# Performance optimization caches for path resolution +_path_cache: dict[int, pathlib.Path] = {} # Cache node paths by object id +_path_to_str_cache: dict[pathlib.Path, str] = {} # Cache path-to-string conversions +_CACHED_CWD: pathlib.Path | None = None + + +def get_test_root_path() -> pathlib.Path: + """Get the root path for the test tree. + + For project-based testing, this returns PROJECT_ROOT_PATH (the project root). + For legacy mode, this returns the current working directory. + + Returns: + pathlib.Path: The root path to use for the test tree. + """ + if PROJECT_ROOT_PATH: + return pathlib.Path(PROJECT_ROOT_PATH) + return pathlib.Path.cwd() + + +def pytest_load_initial_conftests(early_config, parser, args): # noqa: ARG001 + has_pytest_cov = early_config.pluginmanager.hasplugin( + "pytest_cov" + ) or early_config.pluginmanager.hasplugin("pytest_cov.plugin") + has_cov_arg = any("--cov" in arg for arg in args) + if has_cov_arg and not has_pytest_cov: + raise VSCodePytestError( + "\n \nERROR: pytest-cov is not installed, please install this before running pytest with coverage as pytest-cov is required. \n" + ) + if "--cov-branch" in args: + global INCLUDE_BRANCHES + INCLUDE_BRANCHES = True + + global TEST_RUN_PIPE + TEST_RUN_PIPE = os.getenv("TEST_RUN_PIPE") + error_string = ( + "PYTEST ERROR: TEST_RUN_PIPE is not set at the time of pytest starting. " + "Please confirm this environment variable is not being changed or removed " + "as it is required for successful test discovery and execution." + f"TEST_RUN_PIPE = {TEST_RUN_PIPE}\n" + ) + if not TEST_RUN_PIPE: + print(error_string, file=sys.stderr) + if "--collect-only" in args: + global IS_DISCOVERY + IS_DISCOVERY = True + + # check if --rootdir is in the args + for arg in args: + if "--rootdir=" in arg: + rootdir = pathlib.Path(arg.split("--rootdir=")[1]) + if not rootdir.exists(): + raise VSCodePytestError( + f"The path set in the argument --rootdir={rootdir} does not exist." + ) + + # Check if the rootdir is a symlink or a child of a symlink to the current cwd. + is_symlink = False + + if rootdir.is_symlink(): + is_symlink = True + print( + f"Plugin info[vscode-pytest]: rootdir argument, {rootdir}, is identified as a symlink." + ) + elif rootdir.resolve() != rootdir: + print("Plugin info[vscode-pytest]: Checking if rootdir is a child of a symlink.") + is_symlink = has_symlink_parent(rootdir) + if is_symlink: + print( + f"Plugin info[vscode-pytest]: rootdir argument, {rootdir}, is identified as a symlink or child of a symlink, adjusting pytest paths accordingly.", + ) + global SYMLINK_PATH + SYMLINK_PATH = rootdir + + +def pytest_internalerror(excrepr, excinfo): # noqa: ARG001 + """A pytest hook that is called when an internal error occurs. + + Keyword arguments: + excrepr -- the exception representation. + excinfo -- the exception information of type ExceptionInfo. + """ + # call.excinfo.exconly() returns the exception as a string. + ERRORS.append(excinfo.exconly() + "\n Check Python Logs for more details.") + + +def pytest_exception_interact(node, call, report): + """A pytest hook that is called when an exception is raised which could be handled. + + Keyword arguments: + node -- the node that raised the exception. + call -- the call object. + report -- the report object of either type CollectReport or TestReport. + """ + # call.excinfo is the captured exception of the call, if it raised as type ExceptionInfo. + # call.excinfo.exconly() returns the exception as a string. + # If it is during discovery, then add the error to error logs. + if IS_DISCOVERY: + if call.excinfo and call.excinfo.typename != "AssertionError": + if report.outcome == "skipped" and "SkipTest" in str(call): + return + ERRORS.append(call.excinfo.exconly() + "\n Check Python Logs for more details.") + else: + ERRORS.append(report.longreprtext + "\n Check Python Logs for more details.") + else: + # If during execution, send this data that the given node failed. + report_value = "error" + if call.excinfo.typename == "AssertionError": + report_value = "failure" + node_id = get_absolute_test_id(node.nodeid, get_node_path(node)) + if node_id not in collected_tests_so_far: + collected_tests_so_far.add(node_id) + item_result = create_test_outcome( + node_id, + report_value, + "Test failed with exception", + report.longreprtext, + ) + collected_test = TestRunResultDict() + collected_test[node_id] = item_result + cwd = pathlib.Path.cwd() + send_execution_message( + os.fsdecode(cwd), + "success", + collected_test or None, + ) + + +def has_symlink_parent(current_path): + """Recursively checks if any parent directories of the given path are symbolic links.""" + # Convert the current path to an absolute Path object + curr_path = pathlib.Path(current_path) + print("Checking for symlink parent starting at current path: ", curr_path) + + # Iterate over all parent directories + for parent in curr_path.parents: + # Check if the parent directory is a symlink + if parent.is_symlink(): + print(f"Symlink found at: {parent}") + return True + return False + + +def get_absolute_test_id(test_id: str, test_path: pathlib.Path) -> str: + """A function that returns the absolute test id. + + This is necessary because testIds are relative to the rootdir. + This does not work for our case since testIds when referenced during run time are relative to the instantiation + location. Absolute paths for testIds are necessary for the test tree ensures configurations that change the rootdir + of pytest are handled correctly. + + Keyword arguments: + test_id -- the pytest id of the test which is relative to the rootdir. + testPath -- the path to the file the test is located in, as a pathlib.Path object. + """ + split_id = test_id.split("::")[1:] + return "::".join([str(test_path), *split_id]) + + +def pytest_keyboard_interrupt(excinfo): + """A pytest hook that is called when a keyboard interrupt is raised. + + Keyword arguments: + excinfo -- the exception information of type ExceptionInfo. + """ + # The function execonly() returns the exception as a string. + ERRORS.append(excinfo.exconly() + "\n Check Python Logs for more details.") + + +class TestOutcome(Dict): + """A class that handles outcome for a single test. + + for pytest the outcome for a test is only 'passed', 'skipped' or 'failed' + """ + + test: str + outcome: Literal["success", "failure", "skipped", "error"] + message: str | None + traceback: str | None + subtest: str | None + + +def create_test_outcome( + testid: str, + outcome: str, + message: str | None, + traceback: str | None, + subtype: str | None = None, # noqa: ARG001 +) -> TestOutcome: + """A function that creates a TestOutcome object.""" + return TestOutcome( + test=testid, + outcome=outcome, + message=message, + traceback=traceback, # TODO: traceback + subtest=None, + ) + + +class TestRunResultDict(Dict[str, Dict[str, TestOutcome]]): + """A class that stores all test run results.""" + + outcome: str + tests: dict[str, TestOutcome] + + +@pytest.hookimpl(hookwrapper=True, trylast=True) +def pytest_report_teststatus(report, config): # noqa: ARG001 + """A pytest hook that is called when a test is called. + + It is called 3 times per test, during setup, call, and teardown. + + Keyword arguments: + report -- the report on the test setup, call, and teardown. + config -- configuration object. + """ + cwd = pathlib.Path.cwd() + if SYMLINK_PATH: + cwd = SYMLINK_PATH + + if report.when == "call" or (report.when == "setup" and report.skipped): + traceback = None + message = None + report_value = "skipped" + if report.passed: + report_value = "success" + elif report.failed: + report_value = "failure" + message = report.longreprtext + try: + node_path = map_id_to_path[report.nodeid] + except KeyError: + node_path = cwd + # Calculate the absolute test id and use this as the ID moving forward. + absolute_node_id = get_absolute_test_id(report.nodeid, node_path) + if absolute_node_id not in collected_tests_so_far: + collected_tests_so_far.add(absolute_node_id) + item_result = create_test_outcome( + absolute_node_id, + report_value, + message, + traceback, + ) + collected_test = TestRunResultDict() + collected_test[absolute_node_id] = item_result + send_execution_message( + os.fsdecode(cwd), + "success", + collected_test or None, + ) + yield + + +ERROR_MESSAGE_CONST = { + 2: "Pytest was unable to start or run any tests due to issues with test discovery or test collection.", + 3: "Pytest was interrupted by the user, for example by pressing Ctrl+C during test execution.", + 4: "Pytest encountered an internal error or exception during test execution.", + 5: "Pytest was unable to find any tests to run.", +} + + +@pytest.hookimpl(hookwrapper=True, trylast=True) +def pytest_runtest_protocol(item, nextitem): # noqa: ARG001 + map_id_to_path[item.nodeid] = get_node_path(item) + skipped = check_skipped_wrapper(item) + if skipped: + absolute_node_id = get_absolute_test_id(item.nodeid, get_node_path(item)) + report_value = "skipped" + cwd = pathlib.Path.cwd() + if absolute_node_id not in collected_tests_so_far: + collected_tests_so_far.add(absolute_node_id) + item_result = create_test_outcome( + absolute_node_id, + report_value, + None, + None, + ) + collected_test = TestRunResultDict() + collected_test[absolute_node_id] = item_result + send_execution_message( + os.fsdecode(cwd), + "success", + collected_test or None, + ) + yield + + +def check_skipped_wrapper(item): + """A function that checks if a test is skipped or not by check its markers and its parent markers. + + Returns True if the test is marked as skipped at any level, False otherwise. + + Keyword arguments: + item -- the pytest item object. + """ + if item.own_markers and check_skipped_condition(item): + return True + parent = item.parent + while isinstance(parent, pytest.Class): + if parent.own_markers and check_skipped_condition(parent): + return True + parent = parent.parent + return False + + +def check_skipped_condition(item): + """A helper function that checks if a item has a skip or a true skip condition. + + Keyword arguments: + item -- the pytest item object. + """ + for marker in item.own_markers: + # If the test is marked with skip then it will not hit the pytest_report_teststatus hook, + # therefore we need to handle it as skipped here. + skip_condition = False + if marker.name == "skipif": + skip_condition = any(marker.args) + if marker.name == "skip" or skip_condition: + return True + return False + + +class FileCoverageInfo(TypedDict): + lines_covered: list[int] + lines_missed: list[int] + executed_branches: int + total_branches: int + + +def pytest_sessionfinish(session, exitstatus): + """A pytest hook that is called after pytest has fulled finished. + + Keyword arguments: + session -- the pytest session object. + exitstatus -- the status code of the session. + + Exit code 0: All tests were collected and passed successfully + Exit code 1: Tests were collected and run but some of the tests failed + Exit code 2: Test execution was interrupted by the user + Exit code 3: Internal error happened while executing tests + Exit code 4: pytest command line usage error + Exit code 5: No tests were collected + """ + # Get the root path for the test tree structure (not the CWD for test execution) + # This is PROJECT_ROOT_PATH in project-based mode, or cwd in legacy mode + test_root_path = get_test_root_path() + if SYMLINK_PATH: + print("Plugin warning[vscode-pytest]: SYMLINK set, adjusting test root path.") + test_root_path = pathlib.Path(SYMLINK_PATH) + + if IS_DISCOVERY: + if not (exitstatus == 0 or exitstatus == 1 or exitstatus == 5): + error_node: TestNode = { + "name": "", + "path": test_root_path, + "type_": "error", + "children": [], + "id_": "", + } + send_discovery_message(os.fsdecode(test_root_path), error_node) + try: + session_node: TestNode | None = build_test_tree(session) + if not session_node: + raise VSCodePytestError( + "Something went wrong following pytest finish, \ + no session node was created" + ) + send_discovery_message(os.fsdecode(test_root_path), session_node) + except Exception as e: + ERRORS.append( + f"Error Occurred, traceback: {(traceback.format_exc() if e.__traceback__ else '')}" + ) + error_node: TestNode = { + "name": "", + "path": test_root_path, + "type_": "error", + "children": [], + "id_": "", + } + send_discovery_message(os.fsdecode(test_root_path), error_node) + else: + if exitstatus == 0 or exitstatus == 1: + exitstatus_bool = "success" + else: + ERRORS.append( + f"Pytest exited with error status: {exitstatus}, {ERROR_MESSAGE_CONST[exitstatus]}" + ) + exitstatus_bool = "error" + + send_execution_message( + os.fsdecode(test_root_path), + exitstatus_bool, + None, + ) + # send end of transmission token + + # send coverage if enabled + is_coverage_run = os.environ.get("COVERAGE_ENABLED") + if is_coverage_run == "True": + # load the report and build the json result to return + import coverage + + # insert "python_files/lib/python" into the path so packaging can be imported + python_files_dir = pathlib.Path(__file__).parent.parent + bundled_dir = pathlib.Path(python_files_dir / "lib" / "python") + sys.path.append(os.fspath(bundled_dir)) + + from packaging.version import Version + + coverage_version = Version(coverage.__version__) + global INCLUDE_BRANCHES + # only include branches if coverage version is 7.7.0 or greater (as this was when the api saves) + if coverage_version < Version("7.7.0") and INCLUDE_BRANCHES: + print( + "Plugin warning[vscode-pytest]: Branch coverage not supported in this coverage versions < 7.7.0. Please upgrade coverage package if you would like to see branch coverage." + ) + INCLUDE_BRANCHES = False + + try: + from coverage.exceptions import NoSource + except ImportError: + from coverage.misc import NoSource + + cov = coverage.Coverage() + cov.load() + + file_set: set[str] = cov.get_data().measured_files() + file_coverage_map: dict[str, FileCoverageInfo] = {} + + # remove files omitted per coverage report config if any + omit_files: list[str] | None = cov.config.report_omit + if omit_files is not None: + for pattern in omit_files: + for file in list(file_set): + if pathlib.Path(file).match(pattern): + file_set.remove(file) + + for file in file_set: + try: + analysis = cov.analysis2(file) + taken_file_branches = 0 + total_file_branches = -1 + + if INCLUDE_BRANCHES: + branch_stats: dict[int, tuple[int, int]] = cov.branch_stats(file) + total_file_branches = sum( + [total_exits for total_exits, _ in branch_stats.values()] + ) + taken_file_branches = sum( + [taken_exits for _, taken_exits in branch_stats.values()] + ) + + except NoSource: + # as per issue 24308 this best way to handle this edge case + continue + except Exception as e: + print( + f"Plugin error[vscode-pytest]: Skipping analysis of file: {file} due to error: {e}" + ) + continue + lines_executable = {int(line_no) for line_no in analysis[1]} + lines_missed = {int(line_no) for line_no in analysis[3]} + lines_covered = lines_executable - lines_missed + file_info: FileCoverageInfo = { + "lines_covered": list(lines_covered), # list of int + "lines_missed": list(lines_missed), # list of int + "executed_branches": taken_file_branches, + "total_branches": total_file_branches, + } + # convert relative path to absolute path + if not pathlib.Path(file).is_absolute(): + file = str(pathlib.Path(file).resolve()) + file_coverage_map[file] = file_info + + payload: CoveragePayloadDict = CoveragePayloadDict( + coverage=True, + cwd=os.fspath(test_root_path), + result=file_coverage_map, + error=None, + ) + send_message(payload) + + +def construct_nested_folders( + file_nodes_dict: dict[str, TestNode], + session_node: TestNode, + session_children_dict: dict[str, TestNode], +) -> dict[str, TestNode]: + """Iterate through all files and construct them into nested folders. + + Keyword arguments: + file_nodes_dict -- Dictionary of all file nodes + session_node -- The session node that will be parent to the folder structure + session_children_dict -- Dictionary of session's children nodes indexed by ID + + Returns: + dict[str, TestNode] -- Updated session_children_dict with folder nodes added + """ + created_files_folders_dict: dict[str, TestNode] = {} + for file_node in file_nodes_dict.values(): + # Iterate through all the files that exist and construct them into nested folders. + root_folder_node: TestNode + try: + root_folder_node: TestNode = build_nested_folders( + file_node, created_files_folders_dict, session_node + ) + except ValueError: + # This exception is raised when the session node is not a parent of the file node. + print( + "[vscode-pytest]: Session path not a parent of test paths, adjusting session node to common parent." + ) + file_path_str: str = str(file_node["path"]) + session_path_str: str = str(session_node["path"]) + common_parent = os.path.commonpath([file_path_str, session_path_str]) + common_parent_path = pathlib.Path(common_parent) + print("[vscode-pytest]: Session node now set to: ", common_parent) + session_node["path"] = common_parent_path # pathlib.Path + session_node["id_"] = common_parent # str + session_node["name"] = common_parent_path.name # str + root_folder_node = build_nested_folders( + file_node, created_files_folders_dict, session_node + ) + # The final folder we get to is the highest folder in the path + # and therefore we add this as a child to the session. + root_id = root_folder_node.get("id_") + if root_id and root_id not in session_children_dict: + session_children_dict[root_id] = root_folder_node + + return session_children_dict + + +def process_parameterized_test( + test_case: pytest.Item, + test_node: TestItem, + function_nodes_dict: dict[str, TestNode], + file_nodes_dict: dict[str, TestNode], +) -> TestNode: + """Process a parameterized test case and create appropriate function nodes. + + Keyword arguments: + test_case -- the parameterized pytest test case; must have callspec attribute + test_node -- the test node created from the test case + function_nodes_dict -- dictionary of function nodes indexed by ID + file_nodes_dict -- dictionary of file nodes indexed by path + + Returns: + TestNode -- the node to use for further processing (function node or original test node) + """ + function_name: str = "" + # parameterized test cases cut the repetitive part of the name off. + parent_part, parameterized_section = test_node["name"].split("[", 1) + test_node["name"] = "[" + parameterized_section + + first_split = test_case.nodeid.rsplit( + "::", 1 + ) # splits the parameterized test name from the rest of the nodeid + second_split = first_split[0].rsplit( + ".py", 1 + ) # splits the file path from the rest of the nodeid + + class_and_method = second_split[1] + "::" # This has "::" separator at both ends + # construct the parent id, so it is absolute path :: any class and method :: parent_part + parent_id = cached_fsdecode(get_node_path(test_case)) + class_and_method + parent_part + + try: + function_name = test_case.originalname # type: ignore + except AttributeError: # actual error has occurred + ERRORS.append( + f"unable to find original name for {test_case.name} with parameterization detected." + ) + raise VSCodePytestError( + "Unable to find original name for parameterized test case" + ) from None + + function_test_node = function_nodes_dict.get(parent_id) + if function_test_node is None: + function_test_node = create_parameterized_function_node( + function_name, get_node_path(test_case), parent_id + ) + function_nodes_dict[parent_id] = function_test_node + + if test_node not in function_test_node["children"]: + function_test_node["children"].append(test_node) + + # Check if the parent node of the function is file, if so create/add to this file node. + if isinstance(test_case.parent, pytest.File): + # calculate the parent path of the test case + parent_path = get_node_path(test_case.parent) + parent_path_key = cached_fsdecode(parent_path) + parent_test_case = file_nodes_dict.get(parent_path_key) + if parent_test_case is None: + parent_test_case = create_file_node(parent_path) + file_nodes_dict[parent_path_key] = parent_test_case + if function_test_node not in parent_test_case["children"]: + parent_test_case["children"].append(function_test_node) + + # Return the function node as the test node to handle subsequent nesting + return function_test_node + + +def build_test_tree(session: pytest.Session) -> TestNode: + """Builds a tree made up of testing nodes from the pytest session. + + Keyword arguments: + session -- the pytest session object that contains test items. + + Returns: + TestNode -- The root node of the constructed test tree. + """ + session_node = create_session_node(session) + session_children_dict: dict[str, TestNode] = {} + file_nodes_dict: dict[str, TestNode] = {} + class_nodes_dict: dict[str, TestNode] = {} + function_nodes_dict: dict[str, TestNode] = {} + + # Check to see if the global variable for symlink path is set + if SYMLINK_PATH: + session_node["path"] = SYMLINK_PATH + session_node["id_"] = os.fspath(SYMLINK_PATH) + + for test_case in session.items: + test_node = create_test_node(test_case) + if hasattr(test_case, "callspec"): # This means it is a parameterized test. + # Process parameterized test and get the function node to use for further processing + test_node = process_parameterized_test( + test_case, test_node, function_nodes_dict, file_nodes_dict + ) + if isinstance(test_case.parent, pytest.Class) or ( + USES_PYTEST_DESCRIBE and isinstance(test_case.parent, DescribeBlock) + ): + case_iter = test_case.parent + node_child_iter = test_node + test_class_node: TestNode | None = None + while isinstance(case_iter, pytest.Class) or ( + USES_PYTEST_DESCRIBE and isinstance(case_iter, DescribeBlock) + ): + # While the given node is a class, create a class and nest the previous node as a child. + test_class_node = class_nodes_dict.get(case_iter.nodeid) + if test_class_node is None: + test_class_node = create_class_node(case_iter) + class_nodes_dict[case_iter.nodeid] = test_class_node + # Check if the class already has the child node. This will occur if the test is parameterized. + if node_child_iter not in test_class_node["children"]: + test_class_node["children"].append(node_child_iter) + # Iterate up. + node_child_iter = test_class_node + case_iter = case_iter.parent + # Now the parent node is not a class node, it is a file node. + if case_iter: + parent_module = case_iter + else: + ERRORS.append(f"Test class {case_iter} has no parent") + break + parent_path = get_node_path(parent_module) + # Create a file node that has the last class as a child. + parent_path_key = cached_fsdecode(parent_path) + test_file_node = file_nodes_dict.get(parent_path_key) + if test_file_node is None: + test_file_node = create_file_node(parent_path) + file_nodes_dict[parent_path_key] = test_file_node + # Check if the class is already a child of the file node. + if test_class_node is not None and test_class_node not in test_file_node["children"]: + test_file_node["children"].append(test_class_node) + elif not hasattr(test_case, "callspec"): + # This includes test cases that are pytest functions or a doctests. + if test_case.parent is None: + ERRORS.append(f"Test case {test_case.name} has no parent") + continue + parent_path = get_node_path( + cast( + "pytest.Session | pytest.Item | pytest.File | pytest.Class | pytest.Module | HasPathOrFspath", + test_case.parent, + ) + ) + parent_path_key = cached_fsdecode(parent_path) + parent_test_case = file_nodes_dict.get(parent_path_key) + if parent_test_case is None: + parent_test_case = create_file_node(parent_path) + file_nodes_dict[parent_path_key] = parent_test_case + parent_test_case["children"].append(test_node) + # Process all files and construct them into nested folders + session_children_dict = construct_nested_folders( + file_nodes_dict, session_node, session_children_dict + ) + session_node["children"] = list(session_children_dict.values()) + return session_node + + +def build_nested_folders( + file_node: TestNode, + created_files_folders_dict: dict[str, TestNode], + session_node: TestNode, +) -> TestNode: + """Takes a file or folder and builds the nested folder structure for it. + + Keyword arguments: + file_module -- the created module for the file we are nesting. + file_node -- the file node that we are building the nested folders for. + created_files_folders_dict -- Dictionary of all the folders and files that have been created where the key is the path. + session -- the pytest session object. + """ + # check if session node is a parent of the file node, throw error if not. + session_node_path = session_node["path"] + is_relative = False + try: + is_relative = file_node["path"].is_relative_to(session_node_path) + except AttributeError: + is_relative = file_node["path"].relative_to(session_node_path) + if not is_relative: + # If the session node is not a parent of the file node, we need to find their common parent. + raise ValueError("session and file not relative to each other, fixing now....") + + # Begin the iterator_path one level above the current file. + prev_folder_node = file_node + iterator_path = file_node["path"].parent + counter = 0 + max_iter = 100 + while iterator_path != session_node_path: + curr_folder_name = iterator_path.name + iterator_path_key = cached_fsdecode(iterator_path) + curr_folder_node = created_files_folders_dict.get(iterator_path_key) + if curr_folder_node is None: + curr_folder_node = create_folder_node(curr_folder_name, iterator_path) + created_files_folders_dict[iterator_path_key] = curr_folder_node + if prev_folder_node not in curr_folder_node["children"]: + curr_folder_node["children"].append(prev_folder_node) + iterator_path = iterator_path.parent + prev_folder_node = curr_folder_node + # Handles error where infinite loop occurs. + counter += 1 + if counter > max_iter: + raise ValueError( + "[vscode-pytest]: Infinite loop occurred in build_nested_folders. iterator_path: ", + iterator_path, + "session_node_path: ", + session_node_path, + ) + return prev_folder_node + + +def create_test_node( + test_case: pytest.Item, +) -> TestItem: + """Creates a test node from a pytest test case. + + Keyword arguments: + test_case -- the pytest test case. + """ + test_case_loc: str = ( + str(test_case.location[1] + 1) if (test_case.location[1] is not None) else "" + ) + absolute_test_id = get_absolute_test_id(test_case.nodeid, get_node_path(test_case)) + return { + "name": test_case.name, + "path": get_node_path(test_case), + "lineno": test_case_loc, + "type_": "test", + "id_": absolute_test_id, + "runID": absolute_test_id, + } + + +def create_session_node(session: pytest.Session) -> TestNode: + """Creates a session node from a pytest session. + + Keyword arguments: + session -- the pytest session. + """ + # Use PROJECT_ROOT_PATH if set (project-based testing), otherwise use session path (legacy) + node_path = pathlib.Path(PROJECT_ROOT_PATH) if PROJECT_ROOT_PATH else get_node_path(session) + return { + "name": node_path.name, + "path": node_path, + "type_": "folder", + "children": [], + "id_": os.fspath(node_path), + } + + +def create_class_node(class_module: pytest.Class | DescribeBlock) -> TestNode: + """Creates a class node from a pytest class object. + + Keyword arguments: + class_module -- the pytest object representing a class module. + """ + # Get line number for the class definition + class_line = "" + try: + if hasattr(class_module, "obj"): + import inspect + + _, lineno = inspect.getsourcelines(class_module.obj) + class_line = str(lineno) + except (OSError, TypeError): + # If we can't get the source lines, leave lineno empty + pass + + return { + "name": class_module.name, + "path": get_node_path(class_module), + "type_": "class", + "children": [], + "id_": get_absolute_test_id(class_module.nodeid, get_node_path(class_module)), + "lineno": class_line, + } + + +def create_parameterized_function_node( + function_name: str, test_path: pathlib.Path, function_id: str +) -> TestNode: + """Creates a function node to be the parent for the parameterized test nodes. + + Keyword arguments: + function_name -- the name of the function. + test_path -- the path to the test file. + function_id -- the previously constructed function id that fits the pattern- absolute path :: any class and method :: parent_part + must be edited to get a unique id for the function node. + """ + return { + "name": function_name, + "path": test_path, + "type_": "function", + "children": [], + "id_": function_id, + } + + +def create_file_node(calculated_node_path: pathlib.Path) -> TestNode: + """Creates a file node from a path which has already been calculated using the get_node_path function. + + Keyword arguments: + calculated_node_path -- the pytest file path. + """ + return { + "name": calculated_node_path.name, + "path": calculated_node_path, + "type_": "file", + "id_": os.fspath(calculated_node_path), + "children": [], + } + + +def create_folder_node(folder_name: str, path_iterator: pathlib.Path) -> TestNode: + """Creates a folder node from a pytest folder name and its path. + + Keyword arguments: + folderName -- the name of the folder. + path_iterator -- the path of the folder. + """ + return { + "name": folder_name, + "path": path_iterator, + "type_": "folder", + "id_": os.fspath(path_iterator), + "children": [], + } + + +class DiscoveryPayloadDict(TypedDict): + """A dictionary that is used to send a post request to the server.""" + + cwd: str + status: Literal["success", "error"] + tests: TestNode | None + error: list[str] | None + + +class ExecutionPayloadDict(Dict): + """A dictionary that is used to send a execution post request to the server.""" + + cwd: str + status: Literal["success", "error"] + result: TestRunResultDict | None + not_found: list[str] | None # Currently unused need to check + error: str | None # Currently unused need to check + + +class CoveragePayloadDict(Dict): + """A dictionary that is used to send a execution post request to the server.""" + + coverage: bool + cwd: str + result: dict[str, FileCoverageInfo] | None + error: str | None # Currently unused need to check + + +def cached_fsdecode(path: pathlib.Path) -> str: + """Convert path to string with caching for performance. + + This function caches path-to-string conversions to avoid redundant + os.fsdecode() calls during test tree building. + + Parameters: + path: The pathlib.Path object to convert to string. + + Returns: + str: The string representation of the path. + """ + if path not in _path_to_str_cache: + _path_to_str_cache[path] = os.fspath(path) + return _path_to_str_cache[path] + + +def get_node_path( + node: pytest.Session + | pytest.Item + | pytest.File + | pytest.Class + | pytest.Module + | HasPathOrFspath, +) -> pathlib.Path: + """A function that returns the path of a node given the switch to pathlib.Path. + + It also evaluates if the node is a symlink and returns the equivalent path. + + Parameters: + node: A pytest object or any object that has a path or fspath attribute. + Do NOT pass a pathlib.Path object directly; use it directly instead. + + Returns: + pathlib.Path: The resolved path for the node. + """ + cache_key = id(node) + if cache_key in _path_cache: + return _path_cache[cache_key] + + node_path = getattr(node, "path", None) + if node_path is None: + fspath = getattr(node, "fspath", None) + node_path = pathlib.Path(fspath) if fspath is not None else None + + if not node_path: + raise VSCodePytestError( + f"Unable to find path for node: {node}, node.path: {node.path}, node.fspath: {node.fspath}" + ) + + # Check for the session node since it has the symlink already. + if SYMLINK_PATH and not isinstance(node, pytest.Session): + # Get relative between the cwd (resolved path) and the node path. + try: + # Check to see if the node path contains the symlink root already + # Convert Path objects to strings for os.path.commonpath + symlink_str: str = str(SYMLINK_PATH) + node_path_str: str = str(node_path) + common_path = os.path.commonpath([symlink_str, node_path_str]) + if common_path == os.fsdecode(SYMLINK_PATH): + # The node path is already relative to the SYMLINK_PATH root therefore return + result = node_path + else: + # If the node path is not a symlink, then we need to calculate the equivalent symlink path + # get the relative path between the cwd and the node path (as the node path is not a symlink). + # Use cached cwd to avoid repeated system calls + global _CACHED_CWD + if _CACHED_CWD is None: + _CACHED_CWD = pathlib.Path.cwd() + rel_path = node_path.relative_to(_CACHED_CWD) + # combine the difference between the cwd and the node path with the symlink path + result = pathlib.Path(SYMLINK_PATH, rel_path) + except Exception as e: + raise VSCodePytestError( + f"Error occurred while calculating symlink equivalent from node path: {e}" + f"\n SYMLINK_PATH: {SYMLINK_PATH}, \n node path: {node_path}, \n cwd: {_CACHED_CWD or pathlib.Path.cwd()}" + ) from e + else: + result = node_path + + # Cache before returning + _path_cache[cache_key] = result + return result + + +__writer = None +atexit.register(lambda: __writer.close() if __writer else None) + + +def send_execution_message( + cwd: str, status: Literal["success", "error"], tests: TestRunResultDict | None +): + """Sends message execution payload details. + + Args: + cwd (str): Current working directory. + status (Literal["success", "error"]): Execution status indicating success or error. + tests (Union[testRunResultDict, None]): Test run results, if available. + """ + payload: ExecutionPayloadDict = ExecutionPayloadDict( + cwd=cwd, status=status, result=tests, not_found=None, error=None + ) + if ERRORS: + payload["error"] = ERRORS + send_message(payload) + + +def send_discovery_message(cwd: str, session_node: TestNode) -> None: + """ + Sends a POST request with test session details in payload. + + Args: + cwd (str): Current working directory. + session_node (TestNode): Node information of the test session. + """ + payload: DiscoveryPayloadDict = { + "cwd": cwd, + "status": "success" if not ERRORS else "error", + "tests": session_node, + "error": [], + } + if ERRORS is not None: + payload["error"] = ERRORS + send_message(payload, cls_encoder=PathEncoder) + + +class PathEncoder(json.JSONEncoder): + """A custom JSON encoder that encodes pathlib.Path objects as strings.""" + + def default(self, o): + if isinstance(o, pathlib.Path): + return os.fspath(o) + return super().default(o) + + +def send_message( + payload: ExecutionPayloadDict | DiscoveryPayloadDict | CoveragePayloadDict, + cls_encoder=None, +): + """ + Sends a post request to the server. + + Keyword arguments: + payload -- the payload data to be sent. + cls_encoder -- a custom encoder if needed. + """ + if not TEST_RUN_PIPE: + error_msg = ( + "PYTEST ERROR: TEST_RUN_PIPE is not set at the time of pytest starting. " + "Please confirm this environment variable is not being changed or removed " + "as it is required for successful test discovery and execution." + f"TEST_RUN_PIPE = {TEST_RUN_PIPE}\n" + ) + print(error_msg, file=sys.stderr) + raise VSCodePytestError(error_msg) + + global __writer + + if __writer is None: + try: + __writer = open(TEST_RUN_PIPE, "wb") # noqa: SIM115, PTH123 + except Exception as error: + error_msg = f"Error attempting to connect to extension named pipe {TEST_RUN_PIPE}[vscode-pytest]: {error}" + print(error_msg, file=sys.stderr) + print( + "If you are on a Windows machine, this error may be occurring if any of your tests clear environment variables" + " as they are required to communicate with the extension. Please reference https://docs.pytest.org/en/stable/how-to/monkeypatch.html#monkeypatching-environment-variables" + "for the correct way to clear environment variables during testing.\n", + file=sys.stderr, + ) + __writer = None + raise VSCodePytestError(error_msg) from error + + rpc = { + "jsonrpc": "2.0", + "params": payload, + } + data = json.dumps(rpc, cls=cls_encoder) + try: + if __writer: + request = ( + f"""content-length: {len(data)}\r\ncontent-type: application/json\r\n\r\n{data}""" + ) + size = 4096 + encoded = request.encode("utf-8") + bytes_written = 0 + while bytes_written < len(encoded): + segment = encoded[bytes_written : bytes_written + size] + bytes_written += __writer.write(segment) + __writer.flush() + else: + print( + f"Plugin error connection error[vscode-pytest], writer is None \n[vscode-pytest] data: \n{data} \n", + file=sys.stderr, + ) + except Exception as error: + print( + f"Plugin error, exception thrown while attempting to send data[vscode-pytest]: {error} \n[vscode-pytest] data: \n{data}\n", + file=sys.stderr, + ) + + +class DeferPlugin: + @pytest.hookimpl(hookwrapper=True) + def pytest_xdist_auto_num_workers( + self, config: pytest.Config + ) -> Generator[None, Result[int], None]: + """Determine how many workers to use based on how many tests were selected in the test explorer.""" + outcome = yield + result = min(outcome.get_result(), len(config.option.file_or_dir)) + if result == 1: + result = 0 + outcome.force_result(result) + + +def pytest_plugin_registered(plugin: object, manager: pytest.PytestPluginManager): + plugin_name = "vscode_xdist" + if ( + # only register the plugin if xdist is enabled: + manager.hasplugin("xdist") + # prevent infinite recursion: + and not isinstance(plugin, DeferPlugin) + # prevent this plugin from being registered multiple times: + and not manager.hasplugin(plugin_name) + ): + manager.register(DeferPlugin(), name=plugin_name) diff --git a/python_files/vscode_pytest/_common.py b/python_files/vscode_pytest/_common.py new file mode 100644 index 000000000000..9f835f555b6e --- /dev/null +++ b/python_files/vscode_pytest/_common.py @@ -0,0 +1,2 @@ +# def send_post_request(): +# return diff --git a/python_files/vscode_pytest/run_pytest_script.py b/python_files/vscode_pytest/run_pytest_script.py new file mode 100644 index 000000000000..50ab12a35423 --- /dev/null +++ b/python_files/vscode_pytest/run_pytest_script.py @@ -0,0 +1,73 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +import os +import pathlib +import sys +import sysconfig + +import pytest + +# Adds the scripts directory to the PATH as a workaround for enabling shell for test execution. +path_var_name = "PATH" if "PATH" in os.environ else "Path" +os.environ[path_var_name] = ( + sysconfig.get_paths()["scripts"] + os.pathsep + os.environ[path_var_name] +) + +script_dir = pathlib.Path(__file__).parent.parent +sys.path.append(os.fspath(script_dir)) +sys.path.append(os.fspath(script_dir / "lib" / "python")) + + +def run_pytest(args): + arg_array = ["-p", "vscode_pytest", *args] + pytest.main(arg_array) + + +# This script handles running pytest via pytest.main(). It is called via run in the +# pytest execution adapter and gets the test_ids to run via stdin and the rest of the +# args through sys.argv. It then runs pytest.main() with the args and test_ids. + +if __name__ == "__main__": + # Add the root directory to the path so that we can import the plugin. + directory_path = pathlib.Path(__file__).parent.parent + sys.path.append(os.fspath(directory_path)) + sys.path.insert(0, os.getcwd()) # noqa: PTH109 + # Get the rest of the args to run with pytest. + args = sys.argv[1:] + + # Check if coverage is enabled and adjust the args accordingly. + is_coverage_run = os.environ.get("COVERAGE_ENABLED") + coverage_enabled = False + if is_coverage_run == "True": + # If coverage is enabled, check if the coverage plugin is already in the args, if so keep user args. + for arg in args: + # if '--cov' is an arg or if '--cov=' is in an arg (check to see if this arg is set to not override user intent) + if arg == "--cov" or "--cov=" in arg: + print("coverage already enabled with specific args") + coverage_enabled = True + break + if not coverage_enabled: + args = [*args, "--cov=.", "--cov-branch"] + + run_test_ids_pipe = os.environ.get("RUN_TEST_IDS_PIPE") + if run_test_ids_pipe: + ids_path = pathlib.Path(run_test_ids_pipe) + try: + # Read the test ids from the file and run pytest. + ids = ids_path.read_text(encoding="utf-8").splitlines() + except Exception as e: + print("Error[vscode-pytest]: unable to read testIds from temp file" + str(e)) + run_pytest(args) + else: + arg_array = ["-p", "vscode_pytest", *args, *ids] + print("Running pytest with args: " + str(arg_array)) + pytest.main(arg_array) + finally: + # Delete the test ids temp file. + try: + ids_path.unlink() + except Exception as e: + print("Error[vscode-pytest]: unable to delete temp file" + str(e)) + else: + print("Error[vscode-pytest]: RUN_TEST_IDS_PIPE env var is not set.") + run_pytest(args) diff --git a/requirements.in b/requirements.in new file mode 100644 index 000000000000..8bbc9a0f3728 --- /dev/null +++ b/requirements.in @@ -0,0 +1,15 @@ +# This file is used to generate requirements.txt. +# To update requirements.txt, run the following commands. +# 1) Install `uv` https://docs.astral.sh/uv/getting-started/installation/ +# 2) uv pip compile --generate-hashes --upgrade requirements.in -o requirements.txt + +# Unittest test adapter +typing-extensions==4.15.0 + +# Fallback env creator for debian +microvenv + +# Checker for installed packages +importlib_metadata +packaging +tomli diff --git a/requirements.txt b/requirements.txt index 94f727a2a9fc..540590ed2ae7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,71 @@ -jedi==0.12.0 -parso==0.2.1 -isort==4.3.4 -ptvsd==4.2.0 +# This file was autogenerated by uv via the following command: +# uv pip compile --generate-hashes requirements.in -o requirements.txt +importlib-metadata==9.0.0 \ + --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \ + --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc + # via -r requirements.in +microvenv==2025.0 \ + --hash=sha256:568155ec18af01c89f270d35d123ab803b09672b480c3702d15fd69e9cc5bd1e \ + --hash=sha256:8a2568a8390a4ffb5af2f05e7642454e03b887e582d192b6316326974eab5d0f + # via -r requirements.in +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 + # via -r requirements.in +tomli==2.4.1 \ + --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ + --hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \ + --hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \ + --hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \ + --hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \ + --hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \ + --hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \ + --hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \ + --hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \ + --hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \ + --hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \ + --hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \ + --hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \ + --hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \ + --hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \ + --hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \ + --hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \ + --hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \ + --hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \ + --hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \ + --hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \ + --hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \ + --hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \ + --hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \ + --hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \ + --hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \ + --hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \ + --hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \ + --hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \ + --hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \ + --hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \ + --hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \ + --hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \ + --hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \ + --hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \ + --hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \ + --hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \ + --hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \ + --hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \ + --hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \ + --hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \ + --hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \ + --hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \ + --hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \ + --hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \ + --hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049 + # via -r requirements.in +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via -r requirements.in +zipp==3.21.0 \ + --hash=sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4 \ + --hash=sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931 + # via importlib-metadata diff --git a/resources/PythonSelector.png b/resources/PythonSelector.png deleted file mode 100644 index e0c94bdb45e6..000000000000 Binary files a/resources/PythonSelector.png and /dev/null differ diff --git a/resources/ctagOptions b/resources/ctagOptions deleted file mode 100644 index 3b656ac370fe..000000000000 --- a/resources/ctagOptions +++ /dev/null @@ -1,23 +0,0 @@ ---recurse=yes ---tag-relative=yes ---exclude=.git ---exclude=log ---exclude=tmp ---exclude=doc ---exclude=deps ---exclude=node_modules ---exclude=.vscode ---exclude=public/assets ---exclude=*.git* ---exclude=*.pyc ---exclude=*.pyo ---exclude=.DS_Store ---exclude=**/*.jar ---exclude=**/*.class ---exclude=**/.idea/ ---exclude=build ---exclude=Builds ---exclude=doc ---fields=Knz ---extra=+f ---append=no \ No newline at end of file diff --git a/resources/dark/debug.svg b/resources/dark/debug.svg new file mode 100644 index 000000000000..ff7828487e9a --- /dev/null +++ b/resources/dark/debug.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8775 4.5V3.91833C10.8775 2.30658 9.57092 1 7.95917 1C6.34742 1 5.04084 2.30658 5.04084 3.91833V4.5H4.20835L2.54527 2.8285L1.95216 3.41862L3.56303 5.03764L3.54447 5.08683C3.22212 5.94055 3.04084 6.90159 3.04084 7.91833C3.04084 8.11403 3.04755 8.30766 3.0607 8.49886L3.0637 8.5425H1V9.37916H3.16882L3.17494 9.41265C3.34718 10.3545 3.6785 11.2152 4.12918 11.9442L4.16317 11.9992L2.19995 13.9624L2.79157 14.554L4.66326 12.6823L4.72075 12.748C5.58881 13.7401 6.72251 14.3367 7.95917 14.3367C9.17697 14.3367 10.2949 13.7582 11.1576 12.7932L11.2153 12.7287L13.1251 14.6481L13.7182 14.058L11.7218 12.0515L11.7565 11.9964C12.2239 11.2564 12.567 10.3771 12.7434 9.41265L12.7495 9.37916H14.92V8.5425H12.8546L12.8576 8.49886C12.8708 8.30766 12.8775 8.11403 12.8775 7.91833C12.8775 6.88815 12.6914 5.91515 12.361 5.05303L12.3421 5.00354L13.9119 3.43371L13.3203 2.8421L11.6624 4.5H10.8775ZM5.87751 4.5V3.91833C5.87751 2.76866 6.8095 1.83667 7.95917 1.83667C9.10884 1.83667 10.0408 2.76866 10.0408 3.91833V4.5H5.87751ZM11.5739 5.33667L11.5938 5.38957C11.8772 6.14269 12.0408 7.00011 12.0408 7.91833C12.0408 9.52826 11.5379 10.9522 10.7668 11.9546C9.99644 12.9561 8.99584 13.5 7.95917 13.5C6.9225 13.5 5.9219 12.9561 5.15153 11.9546C4.38048 10.9522 3.8775 9.52826 3.8775 7.91833C3.8775 7.00011 4.0411 6.1427 4.32451 5.38957L4.34441 5.33667H11.5739Z" fill="#C5C5C5"/> +</svg> diff --git a/resources/dark/discovering-tests.svg b/resources/dark/discovering-tests.svg new file mode 100644 index 000000000000..f8d5f6b8d62b --- /dev/null +++ b/resources/dark/discovering-tests.svg @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> + <defs> + <style type="text/css"><![CDATA[ + g { + transform-origin: 8px 8px; + animation: 1s linear infinite rotate; + } + @keyframes rotate { + from { transform: rotate(0); } + to { transform: rotate(1turn); } + } + ]]></style> + </defs> + <g> + <path d="M14,6 A6.3,6.3 0 1 0 12.5,12.5" style="stroke: #cccccc; stroke-width: 1.5; fill: none;"/> + <path d="M15,2 L15,6.5 L11,6.5" style="stroke: #cccccc; stroke-width: 1.5; fill: none;"/> + </g> +</svg> diff --git a/resources/dark/export_to_python.svg b/resources/dark/export_to_python.svg new file mode 100644 index 000000000000..a68ca2942cb7 --- /dev/null +++ b/resources/dark/export_to_python.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #C5C5C5 !important;" d="M11 7.394L13.0177 9.41086L12.4109 10.0169L11.4286 9.03457V12.2857H8V11.4286H10.5714V9.03457L9.58914 10.0169L8.98229 9.41086L11 7.394ZM14 3.53686V8.85714H13.1429V4.57143H11.4286V2.85714H9.71429V8H8.85714V2H12.4631L14 3.53686ZM12.9654 3.71429L12.2857 3.03457V3.71429H12.9654ZM2 12.2857H7.14286V11.4286H2V12.2857ZM2 14H7.14286V13.1429H2V14ZM2 10.5714H7.14286V9.71429H2V10.5714Z"/> +<path style="fill: #75BEFF !important;" d="M11 7.38538L13.0177 9.40223L12.4109 10.0082L11.4286 9.02595V12.2771H8V11.4199H10.5714V9.02595L9.58914 10.0082L8.98229 9.40223L11 7.38538Z"/> +</svg> diff --git a/resources/dark/open-file.svg b/resources/dark/open-file.svg new file mode 100644 index 000000000000..969f72386706 --- /dev/null +++ b/resources/dark/open-file.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M5.91421 6L8.06065 3.85356L8.06065 3.14645L5.91421 1L5.2071 1.70711L6 2.50001V2.50004L6.49996 3H6.49999L6.99999 3.50001L5.2071 5.29289L5.91421 6ZM5 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" fill="#C4C4C4"/> +<path d="M2 3H7V4H2V3Z" fill="#C5C5C5"/> +</svg> diff --git a/resources/dark/play.svg b/resources/dark/play.svg new file mode 100644 index 000000000000..8b0a58eca9ba --- /dev/null +++ b/resources/dark/play.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M4 2V14.4805L12.9146 8.24024L4 2ZM11.1809 8.24024L4.995 12.5684V3.91209L11.1809 8.24024Z" fill="#C5C5C5"/> +</svg> diff --git a/resources/dark/refresh.svg b/resources/dark/refresh.svg new file mode 100644 index 000000000000..0442b2af7322 --- /dev/null +++ b/resources/dark/refresh.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M5.56253 2.5158C3.46348 3.45013 2 5.55417 2 8.00002C2 11.3137 4.68629 14 8 14C11.3137 14 14 11.3137 14 8.00002C14 5.32522 12.2497 3.05922 9.83199 2.28485L9.52968 3.23835C11.5429 3.88457 13 5.77213 13 8.00002C13 10.7614 10.7614 13 8 13C5.23858 13 3 10.7614 3 8.00002C3 6.31107 3.83742 4.8177 5.11969 3.91248L5.56253 2.5158Z" fill="#C5C5C5"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3H2V2H5.5L6 2.5V6H5V3Z" fill="#C5C5C5"/> +</svg> diff --git a/resources/dark/repl.svg b/resources/dark/repl.svg new file mode 100644 index 000000000000..1e2d3b4ee13d --- /dev/null +++ b/resources/dark/repl.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H15V15H1V1ZM2 14H14V2H2V14ZM4.00008 5.70709L4.70718 4.99999L8.24272 8.53552L7.53561 9.24263L7.53558 9.2426L4.70711 12.0711L4 11.364L6.82848 8.53549L4.00008 5.70709Z" fill="#C5C5C5"/> +</svg> diff --git a/resources/dark/restart-kernel.svg b/resources/dark/restart-kernel.svg new file mode 100644 index 000000000000..720bc53df88c --- /dev/null +++ b/resources/dark/restart-kernel.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #89D185 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M12.75 8C12.75 10.4853 10.7353 12.5 8.24999 12.5C6.41795 12.5 4.84162 11.4052 4.13953 9.83416L2.74882 10.399C3.67446 12.5186 5.78923 14 8.24999 14C11.5637 14 14.25 11.3137 14.25 8C14.25 4.68629 11.5637 2 8.24999 2C6.3169 2 4.59732 2.91418 3.5 4.3338V2.5H2V6.5L2.75 7.25H6.25V5.75H4.35201C5.13008 4.40495 6.58436 3.5 8.24999 3.5C10.7353 3.5 12.75 5.51472 12.75 8Z"/> +</svg> diff --git a/resources/dark/run-failed-tests.svg b/resources/dark/run-failed-tests.svg new file mode 100644 index 000000000000..27b4d3c7fc77 --- /dev/null +++ b/resources/dark/run-failed-tests.svg @@ -0,0 +1,11 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0)"> +<path d="M12 8C11.2089 8 10.4355 8.2346 9.77772 8.67412C9.11993 9.11365 8.60723 9.73836 8.30448 10.4693C8.00173 11.2002 7.92252 12.0044 8.07686 12.7804C8.2312 13.5563 8.61216 14.269 9.17157 14.8284C9.73098 15.3878 10.4437 15.7688 11.2196 15.9231C11.9956 16.0775 12.7998 15.9983 13.5307 15.6955C14.2616 15.3928 14.8864 14.8801 15.3259 14.2223C15.7654 13.5645 16 12.7911 16 12C16 10.9391 15.5786 9.92172 14.8284 9.17157C14.0783 8.42143 13.0609 8 12 8ZM14.35 13.65L13.65 14.35L12 12.71L10.35 14.35L9.65 13.65L11.29 12L9.65 10.35L10.35 9.65L12 11.29L13.65 9.65L14.35 10.35L12.71 12L14.35 13.65Z" fill="#C5C5C5"/> +<path d="M1.8 10.9L1 10.5V1.4L1.8 1L9 5.5V6.3L1.8 10.9ZM2 2.3V9.6L7.8 6L2 2.3Z" fill="#89D185"/> +</g> +<defs> +<clipPath id="clip0"> +<rect width="16" height="16" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/resources/dark/run-file.svg b/resources/dark/run-file.svg new file mode 100644 index 000000000000..53478e4ec0d4 --- /dev/null +++ b/resources/dark/run-file.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M4 2.00005V13.82L13 7.88006L4 2.00005ZM5.5 4.82L10.3101 7.88006L5.5 11.0001V4.82Z" fill="#89D185"/> +</svg> diff --git a/resources/dark/run-tests.svg b/resources/dark/run-tests.svg new file mode 100644 index 000000000000..9ccf37eb6031 --- /dev/null +++ b/resources/dark/run-tests.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M10.9146 8.24024L2 14.4805V2L10.9146 8.24024ZM2.995 12.5684L9.18093 8.24024L2.995 3.91209V12.5684ZM5.5 14.4805V13.2511L12.6809 8.24024L5.5 3.22935V2L14.4146 8.24024L5.5 14.4805Z" fill="#89D185"/> +</svg> diff --git a/resources/dark/start.svg b/resources/dark/start.svg new file mode 100644 index 000000000000..53478e4ec0d4 --- /dev/null +++ b/resources/dark/start.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M4 2.00005V13.82L13 7.88006L4 2.00005ZM5.5 4.82L10.3101 7.88006L5.5 11.0001V4.82Z" fill="#89D185"/> +</svg> diff --git a/resources/dark/status-error.svg b/resources/dark/status-error.svg new file mode 100644 index 000000000000..8caeaf036dd3 --- /dev/null +++ b/resources/dark/status-error.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M8.6 0.999985C10.2 1.09999 11.7 1.89999 12.8 2.99999C14.1 4.39999 14.8 6.09999 14.8 8.09999C14.8 9.69999 14.2 11.2 13.2 12.5C12.2 13.7 10.8 14.6 9.2 14.9C7.6 15.2 6 15 4.6 14.2C3.2 13.4 2.1 12.2 1.5 10.7C0.899997 9.19999 0.799997 7.49999 1.3 5.99999C1.8 4.39999 2.7 3.09999 4.1 2.19999C5.4 1.29999 7 0.899985 8.6 0.999985ZM9.1 13.9C10.4 13.6 11.6 12.9 12.5 11.8C13.3 10.7 13.8 9.39999 13.7 7.99999C13.7 6.39999 13.1 4.79999 12 3.69999C11 2.69999 9.8 2.09999 8.4 1.99999C7.1 1.89999 5.7 2.19999 4.6 2.99999C3.5 3.79999 2.7 4.89999 2.3 6.29999C1.9 7.59999 1.9 8.99999 2.5 10.3C3.1 11.6 4 12.6 5.2 13.3C6.4 14 7.8 14.2 9.1 13.9ZM7.89999 7.5L10.3 5L11 5.7L8.59999 8.2L11 10.7L10.3 11.4L7.89999 8.9L5.49999 11.4L4.79999 10.7L7.19999 8.2L4.79999 5.7L5.49999 5L7.89999 7.5Z" fill="#F48771"/> +</svg> diff --git a/resources/dark/status-ok.svg b/resources/dark/status-ok.svg new file mode 100644 index 000000000000..4e281aac381b --- /dev/null +++ b/resources/dark/status-ok.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M6.26989 10.8698H6.97989L11.5398 6.30996L10.8298 5.59996L6.62989 9.80985L4.71 7.88994L4 8.59994L6.26989 10.8698Z" fill="#89D185"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M8.6 0.999985C10.2 1.09999 11.7 1.89999 12.8 2.99999C14.1 4.39999 14.8 6.09999 14.8 8.09999C14.8 9.69999 14.2 11.2 13.2 12.5C12.2 13.7 10.8 14.6 9.2 14.9C7.6 15.2 6 15 4.6 14.2C3.2 13.4 2.1 12.2 1.5 10.7C0.899997 9.19999 0.799997 7.49999 1.3 5.99999C1.8 4.39999 2.7 3.09999 4.1 2.19999C5.4 1.29999 7 0.899985 8.6 0.999985ZM9.1 13.9C10.4 13.6 11.6 12.9 12.5 11.8C13.3 10.7 13.8 9.39999 13.7 7.99999C13.7 6.39999 13.1 4.79999 12 3.69999C11 2.69999 9.8 2.09999 8.4 1.99999C7.1 1.89999 5.7 2.19999 4.6 2.99999C3.5 3.79999 2.7 4.89999 2.3 6.29999C1.9 7.59999 1.9 8.99999 2.5 10.3C3.1 11.6 4 12.6 5.2 13.3C6.4 14 7.8 14.2 9.1 13.9Z" fill="#89D185"/> +</svg> diff --git a/resources/dark/status-unknown.svg b/resources/dark/status-unknown.svg new file mode 100644 index 000000000000..960fb0aa27fe --- /dev/null +++ b/resources/dark/status-unknown.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M7.50001 1C6.21443 1 4.95772 1.38123 3.8888 2.09546C2.81988 2.80969 1.98679 3.82485 1.49482 5.01257C1.00285 6.20029 0.874083 7.50719 1.12489 8.76807C1.37569 10.0289 1.99478 11.1872 2.90382 12.0962C3.81286 13.0052 4.97107 13.6243 6.23194 13.8751C7.49282 14.1259 8.79972 13.9972 9.98744 13.5052C11.1752 13.0133 12.1903 12.1801 12.9045 11.1112C13.6188 10.0423 14 8.78558 14 7.5C14 5.77609 13.3152 4.1228 12.0962 2.90381C10.8772 1.68482 9.22392 1 7.50001 1ZM7.50001 13C6.41221 13 5.34881 12.6775 4.44434 12.0731C3.53987 11.4688 2.83493 10.6097 2.41865 9.60474C2.00237 8.59974 1.89344 7.4939 2.10566 6.427C2.31788 5.36011 2.84172 4.38015 3.61091 3.61096C4.3801 2.84177 5.36012 2.31793 6.42701 2.10571C7.49391 1.89349 8.59975 2.00242 9.60474 2.4187C10.6097 2.83498 11.4687 3.53987 12.0731 4.44434C12.6774 5.34881 13 6.4122 13 7.5C13 8.95869 12.4206 10.3576 11.3891 11.389C10.3577 12.4205 8.9587 13 7.50001 13ZM9.04999 4.57994C8.87722 4.40004 8.6697 4.25723 8.44 4.16002C8.151 4.04431 7.84117 3.98979 7.53003 3.99999C7.22804 3.9945 6.92825 4.05246 6.65002 4.17003C6.41146 4.27028 6.19928 4.42423 6.03003 4.61998C5.86442 4.8001 5.73536 5.01066 5.65002 5.23998C5.57068 5.47292 5.52028 5.7147 5.5 5.95995H6.72998C6.73725 5.74494 6.82673 5.54098 6.97998 5.39C7.05193 5.31511 7.13924 5.25671 7.2359 5.21874C7.33256 5.18076 7.43629 5.16414 7.53998 5.17003C7.62942 5.15581 7.72056 5.15581 7.81 5.17003C7.89216 5.2011 7.96708 5.24877 8.03003 5.31004C8.0995 5.37016 8.15422 5.4454 8.19 5.53001C8.23097 5.62465 8.25141 5.72683 8.25 5.82994C8.25037 6.00265 8.21283 6.17333 8.14001 6.32994C8.06739 6.49283 7.97681 6.6472 7.87 6.79002L7.52002 7.20995C7.40002 7.33995 7.27998 7.47998 7.16998 7.61998C7.06332 7.75933 6.97279 7.91024 6.90002 8.06993C6.83065 8.22732 6.79648 8.39797 6.79999 8.56993V9.22997H8V8.73998C8.00339 8.59331 8.04105 8.44943 8.10999 8.31993C8.19183 8.17576 8.28551 8.03871 8.39001 7.91002L8.75 7.46996C8.88106 7.31855 9.00134 7.15818 9.10999 6.98998C9.22491 6.81846 9.31894 6.63376 9.39001 6.43993C9.46294 6.23444 9.50013 6.01808 9.5 5.80003C9.50178 5.57285 9.46807 5.34675 9.40002 5.12999C9.32451 4.9235 9.20506 4.7358 9.04999 4.57994ZM6.8 9.82996H7.97V11H6.8V9.82996Z" fill="#C5C5C5"/> +</svg> diff --git a/resources/dark/stop.svg b/resources/dark/stop.svg new file mode 100644 index 000000000000..b30b6e346d6c --- /dev/null +++ b/resources/dark/stop.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M12.8 2.99999C11.7 1.89999 10.2 1.09999 8.6 0.999985C7 0.899985 5.4 1.29999 4.1 2.19999C2.7 3.09999 1.8 4.39999 1.3 5.99999C0.799997 7.49999 0.899997 9.19999 1.5 10.7C2.1 12.2 3.2 13.4 4.6 14.2C6 15 7.6 15.2 9.2 14.9C10.8 14.6 12.2 13.7 13.2 12.5C14.2 11.2 14.8 9.69999 14.8 8.09999C14.8 6.09999 14.1 4.39999 12.8 2.99999ZM12.5 11.8C11.6 12.9 10.4 13.6 9.1 13.9C7.8 14.2 6.4 14 5.2 13.3C4 12.6 3.1 11.6 2.5 10.3C1.9 8.99999 1.9 7.59999 2.3 6.29999C2.7 4.89999 3.5 3.79999 4.6 2.99999C5.7 2.19999 7.1 1.89999 8.4 1.99999C9.8 2.09999 11 2.69999 12 3.69999C13.1 4.79999 13.7 6.39999 13.7 7.99999C13.8 9.39999 13.3 10.7 12.5 11.8Z" fill="#F48771"/> +<path d="M6 6H10V10H6V6Z" fill="#F48771"/> +</svg> diff --git a/resources/dark/trusted.svg b/resources/dark/trusted.svg new file mode 100644 index 000000000000..a841903129b0 --- /dev/null +++ b/resources/dark/trusted.svg @@ -0,0 +1,4 @@ + +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M7.67 14.72H8.38L10.1 13H12.5L13 12.5V10.08L14.74 8.36004V7.65004L13.03 5.93004V3.49004L12.53 3.00004H10.1L8.38 1.29004H7.67L6 3.00004H3.53L3 3.50004V5.93004L1.31 7.65004V8.36004L3 10.08V12.5L3.53 13H6L7.67 14.72ZM6.16 12H4V9.87004L3.88 9.52004L2.37 8.00004L3.85 6.49004L4 6.14004V4.00004H6.16L6.52 3.86004L8 2.35004L9.54 3.86004L9.89 4.00004H12V6.14004L12.17 6.49004L13.69 8.00004L12.14 9.52004L12 9.87004V12H9.89L9.51 12.15L8 13.66L6.52 12.14L6.16 12ZM6.73004 10.4799H7.44004L11.21 6.71L10.5 6L7.09004 9.41991L5.71 8.03984L5 8.74984L6.73004 10.4799Z" fill="#C5C5C5"/> +</svg> diff --git a/resources/dark/un-trusted.svg b/resources/dark/un-trusted.svg new file mode 100644 index 000000000000..f97f5f561d9e --- /dev/null +++ b/resources/dark/un-trusted.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" + xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M7.67 14.72H8.38L10.1 13H12.5L13 12.5V10.08L14.74 8.36004V7.65004L13.03 5.93004V3.49004L12.53 3.00004H10.1L8.38 1.29004H7.67L6 3.00004H3.53L3 3.50004V5.93004L1.31 7.65004V8.36004L3 10.08V12.5L3.53 13H6L7.67 14.72ZM6.16 12H4V9.87004L3.88 9.52004L2.37 8.00004L3.85 6.49004L4 6.14004V4.00004H6.16L6.52 3.86004L8 2.35004L9.54 3.86004L9.89 4.00004H12V6.14004L12.17 6.49004L13.69 8.00004L12.14 9.52004L12 9.87004V12H9.89L9.51 12.15L8 13.66L6.52 12.14L6.16 12ZM7.60288 6.14101C7.54621 6.22606 7.50337 6.32292 7.47465 6.43203C7.44526 6.54079 7.42468 6.64661 7.41299 6.74891L7.40808 6.79186H6.51332L6.51619 6.74069C6.53447 6.41473 6.60464 6.12645 6.72787 5.87687C6.78019 5.76916 6.84468 5.66329 6.92117 5.55926C7.00224 5.45324 7.10082 5.35923 7.21549 5.27864C7.33383 5.19457 7.47018 5.12809 7.62404 5.07882C7.78275 5.02596 7.96334 5 8.16511 5C8.44581 5 8.68831 5.04573 8.89098 5.13921C9.09163 5.22877 9.25661 5.34985 9.38469 5.50292C9.5119 5.65495 9.60489 5.82866 9.66367 6.02356C9.72201 6.21701 9.75116 6.41655 9.75116 6.62201C9.75116 6.81343 9.71872 6.99327 9.65345 7.1611C9.58933 7.32596 9.50992 7.48175 9.41521 7.62841C9.32115 7.77101 9.21774 7.90792 9.10551 8.03835C8.99509 8.16668 8.89371 8.29042 8.80136 8.40959C8.70988 8.52467 8.63315 8.6383 8.57169 8.74952C8.51214 8.85728 8.48332 8.96318 8.48332 9.06777V9.54783H7.59513V9.00932C7.59513 8.84135 7.62627 8.6862 7.68922 8.54455C7.75366 8.4034 7.83362 8.26953 7.92863 8.14386C8.02547 8.01676 8.12847 7.89406 8.2374 7.77606C8.34426 7.66029 8.44365 7.54161 8.53558 7.42003C8.62919 7.30008 8.70548 7.17399 8.76373 7.04293C8.82348 6.9149 8.85397 6.77427 8.85397 6.62201C8.85397 6.50659 8.83811 6.40079 8.80687 6.30424L8.80647 6.303C8.77825 6.20707 8.73432 6.12583 8.67649 6.05974C8.61866 5.99089 8.5471 5.93848 8.46108 5.90241L8.45931 5.90167C8.377 5.86326 8.27937 5.84324 8.16511 5.84324C8.02809 5.84324 7.91599 5.87172 7.82635 5.92596C7.73629 5.9823 7.66193 6.0539 7.60288 6.14101ZM8.5 11H7.6118V10.1118H8.5V11Z" fill="#C5C5C5"/> +</svg> diff --git a/resources/default.launch.json b/resources/default.launch.json deleted file mode 100644 index 10f90e0b4286..000000000000 --- a/resources/default.launch.json +++ /dev/null @@ -1,58 +0,0 @@ -[ - { - "name": "Python: Current File (Integrated Terminal)", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal" - }, - { - "name": "Python: Attach", - "type": "python", - "request": "attach", - "port": 5678, - "host": "localhost" - }, - { - "name": "Python: Module", - "type": "python", - "request": "launch", - "module": "enter-your-module-name-here", - "console": "integratedTerminal" - }, - { - "name": "Python: Django", - "type": "python", - "request": "launch", - "program": "${workspaceFolder}/manage.py", - "console": "integratedTerminal", - "args": [ - "runserver", - "--noreload", - "--nothreading" - ], - "django": true - }, - { - "name": "Python: Flask", - "type": "python", - "request": "launch", - "module": "flask", - "env": { - "FLASK_APP": "app.py" - }, - "args": [ - "run", - "--no-debugger", - "--no-reload" - ], - "jinja": true - }, - { - "name": "Python: Current File (External Terminal)", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "externalTerminal" - } -] diff --git a/resources/defaultTheme.json b/resources/defaultTheme.json deleted file mode 100644 index c73f4e01b0b0..000000000000 --- a/resources/defaultTheme.json +++ /dev/null @@ -1,370 +0,0 @@ -{ - "$schema": "vscode://schemas/color-theme", - "name": "Embedded Light Theme", - "tokenColors": [ - { - "scope": ["meta.embedded", "source.groovy.embedded"], - "settings": { - "foreground": "#000000ff" - } - }, - { - "scope": "emphasis", - "settings": { - "fontStyle": "italic" - } - }, - { - "scope": "strong", - "settings": { - "fontStyle": "bold" - } - }, - { - "scope": "meta.diff.header", - "settings": { - "foreground": "#000080" - } - }, - { - "scope": "comment", - "settings": { - "foreground": "#008000" - } - }, - { - "scope": "constant.language", - "settings": { - "foreground": "#0000ff" - } - }, - { - "scope": [ - "constant.numeric" - ], - "settings": { - "foreground": "#09885a" - } - }, - { - "scope": "constant.regexp", - "settings": { - "foreground": "#811f3f" - } - }, - { - "name": "css tags in selectors, xml tags", - "scope": "entity.name.tag", - "settings": { - "foreground": "#800000" - } - }, - { - "scope": "entity.name.selector", - "settings": { - "foreground": "#800000" - } - }, - { - "scope": "entity.other.attribute-name", - "settings": { - "foreground": "#ff0000" - } - }, - { - "scope": [ - "entity.other.attribute-name.class.css", - "entity.other.attribute-name.class.mixin.css", - "entity.other.attribute-name.id.css", - "entity.other.attribute-name.parent-selector.css", - "entity.other.attribute-name.pseudo-class.css", - "entity.other.attribute-name.pseudo-element.css", - "source.css.less entity.other.attribute-name.id", - "entity.other.attribute-name.attribute.scss", - "entity.other.attribute-name.scss" - ], - "settings": { - "foreground": "#800000" - } - }, - { - "scope": "invalid", - "settings": { - "foreground": "#cd3131" - } - }, - { - "scope": "markup.underline", - "settings": { - "fontStyle": "underline" - } - }, - { - "scope": "markup.bold", - "settings": { - "fontStyle": "bold", - "foreground": "#000080" - } - }, - { - "scope": "markup.heading", - "settings": { - "fontStyle": "bold", - "foreground": "#800000" - } - }, - { - "scope": "markup.italic", - "settings": { - "fontStyle": "italic" - } - }, - { - "scope": "markup.inserted", - "settings": { - "foreground": "#09885a" - } - }, - { - "scope": "markup.deleted", - "settings": { - "foreground": "#a31515" - } - }, - { - "scope": "markup.changed", - "settings": { - "foreground": "#0451a5" - } - }, - { - "scope": [ - "punctuation.definition.quote.begin.markdown", - "punctuation.definition.list.begin.markdown" - ], - "settings": { - "foreground": "#0451a5" - } - }, - { - "scope": "markup.inline.raw", - "settings": { - "foreground": "#800000" - } - }, - { - "name": "brackets of XML/HTML tags", - "scope": "punctuation.definition.tag", - "settings": { - "foreground": "#800000" - } - }, - { - "scope": "meta.preprocessor", - "settings": { - "foreground": "#0000ff" - } - }, - { - "scope": "meta.preprocessor.string", - "settings": { - "foreground": "#a31515" - } - }, - { - "scope": "meta.preprocessor.numeric", - "settings": { - "foreground": "#09885a" - } - }, - { - "scope": "meta.structure.dictionary.key.python", - "settings": { - "foreground": "#0451a5" - } - }, - { - "scope": "storage", - "settings": { - "foreground": "#0000ff" - } - }, - { - "scope": "storage.type", - "settings": { - "foreground": "#0000ff" - } - }, - { - "scope": "storage.modifier", - "settings": { - "foreground": "#0000ff" - } - }, - { - "scope": "string", - "settings": { - "foreground": "#a31515" - } - }, - { - "scope": [ - "string.comment.buffered.block.pug", - "string.quoted.pug", - "string.interpolated.pug", - "string.unquoted.plain.in.yaml", - "string.unquoted.plain.out.yaml", - "string.unquoted.block.yaml", - "string.quoted.single.yaml", - "string.quoted.double.xml", - "string.quoted.single.xml", - "string.unquoted.cdata.xml", - "string.quoted.double.html", - "string.quoted.single.html", - "string.unquoted.html", - "string.quoted.single.handlebars", - "string.quoted.double.handlebars" - ], - "settings": { - "foreground": "#0000ff" - } - }, - { - "scope": "string.regexp", - "settings": { - "foreground": "#811f3f" - } - }, - { - "name": "String interpolation", - "scope": [ - "punctuation.definition.template-expression.begin", - "punctuation.definition.template-expression.end", - "punctuation.section.embedded" - ], - "settings": { - "foreground": "#0000ff" - } - }, - { - "name": "Reset JavaScript string interpolation expression", - "scope": [ - "meta.template.expression" - ], - "settings": { - "foreground": "#000000" - } - }, - { - "scope": [ - "support.constant.property-value", - "support.constant.font-name", - "support.constant.media-type", - "support.constant.media", - "constant.other.color.rgb-value", - "constant.other.rgb-value", - "support.constant.color" - ], - "settings": { - "foreground": "#0451a5" - } - }, - { - "scope": [ - "support.type.vendored.property-name", - "support.type.property-name", - "variable.css", - "variable.scss", - "variable.other.less", - "source.coffee.embedded" - ], - "settings": { - "foreground": "#ff0000" - } - }, - { - "scope": [ - "support.type.property-name.json" - ], - "settings": { - "foreground": "#0451a5" - } - }, - { - "scope": "keyword", - "settings": { - "foreground": "#0000ff" - } - }, - { - "scope": "keyword.control", - "settings": { - "foreground": "#0000ff" - } - }, - { - "scope": "keyword.operator", - "settings": { - "foreground": "#000000" - } - }, - { - "scope": [ - "keyword.operator.new", - "keyword.operator.expression", - "keyword.operator.cast", - "keyword.operator.sizeof", - "keyword.operator.instanceof", - "keyword.operator.logical.python" - ], - "settings": { - "foreground": "#0000ff" - } - }, - { - "scope": "keyword.other.unit", - "settings": { - "foreground": "#09885a" - } - }, - { - "scope": [ - "punctuation.section.embedded.begin.php", - "punctuation.section.embedded.end.php" - ], - "settings": { - "foreground": "#800000" - } - }, - { - "scope": "support.function.git-rebase", - "settings": { - "foreground": "#0451a5" - } - }, - { - "scope": "constant.sha.git-rebase", - "settings": { - "foreground": "#09885a" - } - }, - { - "name": "coloring of the Java import and package identifiers", - "scope": [ - "storage.modifier.import.java", - "variable.language.wildcard.java", - "storage.modifier.package.java" - ], - "settings": { - "foreground": "#000000" - } - }, - { - "name": "this.self", - "scope": "variable.language", - "settings": { - "foreground": "#0000ff" - } - } - ] -} diff --git a/resources/light/debug.svg b/resources/light/debug.svg new file mode 100644 index 000000000000..8510a05d742b --- /dev/null +++ b/resources/light/debug.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8775 4.5V3.91833C10.8775 2.30658 9.57092 1 7.95917 1C6.34742 1 5.04084 2.30658 5.04084 3.91833V4.5H4.20835L2.54527 2.8285L1.95216 3.41862L3.56303 5.03764L3.54447 5.08683C3.22212 5.94055 3.04084 6.90159 3.04084 7.91833C3.04084 8.11403 3.04755 8.30766 3.0607 8.49886L3.0637 8.5425H1V9.37916H3.16882L3.17494 9.41265C3.34718 10.3545 3.6785 11.2152 4.12918 11.9442L4.16317 11.9992L2.19995 13.9624L2.79157 14.554L4.66326 12.6823L4.72075 12.748C5.58881 13.7401 6.72251 14.3367 7.95917 14.3367C9.17697 14.3367 10.2949 13.7582 11.1576 12.7932L11.2153 12.7287L13.1251 14.6481L13.7182 14.058L11.7218 12.0515L11.7565 11.9964C12.2239 11.2564 12.567 10.3771 12.7434 9.41265L12.7495 9.37916H14.92V8.5425H12.8546L12.8576 8.49886C12.8708 8.30766 12.8775 8.11403 12.8775 7.91833C12.8775 6.88815 12.6914 5.91515 12.361 5.05303L12.3421 5.00354L13.9119 3.43371L13.3203 2.8421L11.6624 4.5H10.8775ZM5.87751 4.5V3.91833C5.87751 2.76866 6.8095 1.83667 7.95917 1.83667C9.10884 1.83667 10.0408 2.76866 10.0408 3.91833V4.5H5.87751ZM11.5739 5.33667L11.5938 5.38957C11.8772 6.14269 12.0408 7.00011 12.0408 7.91833C12.0408 9.52826 11.5379 10.9522 10.7668 11.9546C9.99644 12.9561 8.99584 13.5 7.95917 13.5C6.9225 13.5 5.9219 12.9561 5.15153 11.9546C4.38048 10.9522 3.8775 9.52826 3.8775 7.91833C3.8775 7.00011 4.0411 6.1427 4.32451 5.38957L4.34441 5.33667H11.5739Z" fill="#424242"/> +</svg> diff --git a/resources/light/discovering-tests.svg b/resources/light/discovering-tests.svg new file mode 100644 index 000000000000..703944f3577c --- /dev/null +++ b/resources/light/discovering-tests.svg @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"> + <defs> + <style type="text/css"><![CDATA[ + g { + transform-origin: 8px 8px; + animation: 1s linear infinite rotate; + } + @keyframes rotate { + from { transform: rotate(0); } + to { transform: rotate(1turn); } + } + ]]></style> + </defs> + <g> + <path d="M14,6 A6.3,6.3 0 1 0 12.5,12.5" style="stroke: #424242; stroke-width: 1.5; fill: none;"/> + <path d="M15,2 L15,6.5 L11,6.5" style="stroke: #424242; stroke-width: 1.5; fill: none;"/> + </g> +</svg> diff --git a/resources/light/export_to_python.svg b/resources/light/export_to_python.svg new file mode 100644 index 000000000000..873383aaeb21 --- /dev/null +++ b/resources/light/export_to_python.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #424242 !important;" d="M11 7.394L13.0177 9.41086L12.4109 10.0169L11.4286 9.03457V12.2857H8V11.4286H10.5714V9.03457L9.58914 10.0169L8.98229 9.41086L11 7.394ZM14 3.53686V8.85714H13.1429V4.57143H11.4286V2.85714H9.71429V8H8.85714V2H12.4631L14 3.53686ZM12.9654 3.71429L12.2857 3.03457V3.71429H12.9654ZM2 12.2857H7.14286V11.4286H2V12.2857ZM2 14H7.14286V13.1429H2V14ZM2 10.5714H7.14286V9.71429H2V10.5714Z"/> +<path style="fill: #007ACC !important;" d="M11 7.38538L13.0177 9.40223L12.4109 10.0082L11.4286 9.02595V12.2771H8V11.4199H10.5714V9.02595L9.58914 10.0082L8.98229 9.40223L11 7.38538Z"/> +</svg> diff --git a/resources/light/open-file.svg b/resources/light/open-file.svg new file mode 100644 index 000000000000..039384f2e883 --- /dev/null +++ b/resources/light/open-file.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M5.91421 6L8.06065 3.85356L8.06065 3.14645L5.91421 1L5.2071 1.70711L6 2.50001V2.50004L6.49996 3H6.49999L6.99999 3.50001L5.2071 5.29289L5.91421 6ZM5 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" fill="#424242"/> +<path d="M2 3H7V4H2V3Z" fill="#424242"/> +</svg> diff --git a/resources/light/play.svg b/resources/light/play.svg new file mode 100644 index 000000000000..2563bfa114be --- /dev/null +++ b/resources/light/play.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M4 2V14.4805L12.9146 8.24024L4 2ZM11.1809 8.24024L4.995 12.5684V3.91209L11.1809 8.24024Z" fill="#424242"/> +</svg> diff --git a/resources/light/refresh.svg b/resources/light/refresh.svg new file mode 100644 index 000000000000..8ade09dfae59 --- /dev/null +++ b/resources/light/refresh.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M5.56253 2.5158C3.46348 3.45013 2 5.55417 2 8.00002C2 11.3137 4.68629 14 8 14C11.3137 14 14 11.3137 14 8.00002C14 5.32522 12.2497 3.05922 9.83199 2.28485L9.52968 3.23835C11.5429 3.88457 13 5.77213 13 8.00002C13 10.7614 10.7614 13 8 13C5.23858 13 3 10.7614 3 8.00002C3 6.31107 3.83742 4.8177 5.11969 3.91248L5.56253 2.5158Z" fill="#424242"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3H2V2H5.5L6 2.5V6H5V3Z" fill="#424242"/> +</svg> diff --git a/resources/light/repl.svg b/resources/light/repl.svg new file mode 100644 index 000000000000..429cb22b71f5 --- /dev/null +++ b/resources/light/repl.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M1 1H15V15H1V1ZM2 14H14V2H2V14ZM4.00008 5.70709L4.70718 4.99999L8.24272 8.53552L7.53561 9.24263L7.53558 9.2426L4.70711 12.0711L4 11.364L6.82848 8.53549L4.00008 5.70709Z" fill="#424242"/> +</svg> diff --git a/resources/light/restart-kernel.svg b/resources/light/restart-kernel.svg new file mode 100644 index 000000000000..4ce884e15bbe --- /dev/null +++ b/resources/light/restart-kernel.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path style="fill: #388A34 !important; fill-rule: evenodd !important; clip-rule: evenodd !important" d="M12.75 8C12.75 10.4853 10.7353 12.5 8.24999 12.5C6.41795 12.5 4.84162 11.4052 4.13953 9.83416L2.74882 10.399C3.67446 12.5186 5.78923 14 8.24999 14C11.5637 14 14.25 11.3137 14.25 8C14.25 4.68629 11.5637 2 8.24999 2C6.3169 2 4.59732 2.91418 3.5 4.3338V2.5H2V6.5L2.75 7.25H6.25V5.75H4.35201C5.13008 4.40495 6.58436 3.5 8.24999 3.5C10.7353 3.5 12.75 5.51472 12.75 8Z"/> +</svg> diff --git a/resources/light/run-failed-tests.svg b/resources/light/run-failed-tests.svg new file mode 100644 index 000000000000..f522730115b6 --- /dev/null +++ b/resources/light/run-failed-tests.svg @@ -0,0 +1,11 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0)"> +<path d="M12 8C11.2089 8 10.4355 8.2346 9.77772 8.67412C9.11993 9.11365 8.60723 9.73836 8.30448 10.4693C8.00173 11.2002 7.92252 12.0044 8.07686 12.7804C8.2312 13.5563 8.61216 14.269 9.17157 14.8284C9.73098 15.3878 10.4437 15.7688 11.2196 15.9231C11.9956 16.0775 12.7998 15.9983 13.5307 15.6955C14.2616 15.3928 14.8864 14.8801 15.3259 14.2223C15.7654 13.5645 16 12.7911 16 12C16 10.9391 15.5786 9.92172 14.8284 9.17157C14.0783 8.42143 13.0609 8 12 8ZM14.35 13.65L13.65 14.35L12 12.71L10.35 14.35L9.65 13.65L11.29 12L9.65 10.35L10.35 9.65L12 11.29L13.65 9.65L14.35 10.35L12.71 12L14.35 13.65Z" fill="#424242"/> +<path d="M1.8 10.9L1 10.5V1.4L1.8 1L9 5.5V6.3L1.8 10.9ZM2 2.3V9.6L7.8 6L2 2.3Z" fill="#388A34"/> +</g> +<defs> +<clipPath id="clip0"> +<rect width="16" height="16" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/resources/light/run-file.svg b/resources/light/run-file.svg new file mode 100644 index 000000000000..0adc9e411b03 --- /dev/null +++ b/resources/light/run-file.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" + xmlns="http://www.w3.org/2000/svg"> + <path d="M4 2.00005V13.82L13 7.88006L4 2.00005ZM5.5 4.82L10.3101 7.88006L5.5 11.0001V4.82Z" fill="#388A34"/> +</svg> diff --git a/resources/light/run-tests.svg b/resources/light/run-tests.svg new file mode 100644 index 000000000000..317fb9bd7ed3 --- /dev/null +++ b/resources/light/run-tests.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M10.9146 8.24024L2 14.4805V2L10.9146 8.24024ZM2.995 12.5684L9.18093 8.24024L2.995 3.91209V12.5684ZM5.5 14.4805V13.2511L12.6809 8.24024L5.5 3.22935V2L14.4146 8.24024L5.5 14.4805Z" fill="#388A34"/> +</svg> diff --git a/resources/light/start.svg b/resources/light/start.svg new file mode 100644 index 000000000000..f41a5c8fa2d8 --- /dev/null +++ b/resources/light/start.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M4 2.00005V13.82L13 7.88006L4 2.00005ZM5.5 4.82L10.3101 7.88006L5.5 11.0001V4.82Z" fill="#388A34"/> +</svg> diff --git a/resources/light/status-error.svg b/resources/light/status-error.svg new file mode 100644 index 000000000000..bfca80a2d234 --- /dev/null +++ b/resources/light/status-error.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M8.6 0.999985C10.2 1.09999 11.7 1.89999 12.8 2.99999C14.1 4.39999 14.8 6.09999 14.8 8.09999C14.8 9.69999 14.2 11.2 13.2 12.5C12.2 13.7 10.8 14.6 9.2 14.9C7.6 15.2 6 15 4.6 14.2C3.2 13.4 2.1 12.2 1.5 10.7C0.899997 9.19999 0.799997 7.49999 1.3 5.99999C1.8 4.39999 2.7 3.09999 4.1 2.19999C5.4 1.29999 7 0.899985 8.6 0.999985ZM9.1 13.9C10.4 13.6 11.6 12.9 12.5 11.8C13.3 10.7 13.8 9.39999 13.7 7.99999C13.7 6.39999 13.1 4.79999 12 3.69999C11 2.69999 9.8 2.09999 8.4 1.99999C7.1 1.89999 5.7 2.19999 4.6 2.99999C3.5 3.79999 2.7 4.89999 2.3 6.29999C1.9 7.59999 1.9 8.99999 2.5 10.3C3.1 11.6 4 12.6 5.2 13.3C6.4 14 7.8 14.2 9.1 13.9ZM7.89999 7.5L10.3 5L11 5.7L8.59999 8.2L11 10.7L10.3 11.4L7.89999 8.9L5.49999 11.4L4.79999 10.7L7.19999 8.2L4.79999 5.7L5.49999 5L7.89999 7.5Z" fill="#E51400"/> +</svg> diff --git a/resources/light/status-ok.svg b/resources/light/status-ok.svg new file mode 100644 index 000000000000..9487635d69bb --- /dev/null +++ b/resources/light/status-ok.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M6.26989 10.8698H6.97989L11.5398 6.30996L10.8298 5.59996L6.62989 9.80985L4.71 7.88994L4 8.59994L6.26989 10.8698Z" fill="#388A34"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M8.6 0.999985C10.2 1.09999 11.7 1.89999 12.8 2.99999C14.1 4.39999 14.8 6.09999 14.8 8.09999C14.8 9.69999 14.2 11.2 13.2 12.5C12.2 13.7 10.8 14.6 9.2 14.9C7.6 15.2 6 15 4.6 14.2C3.2 13.4 2.1 12.2 1.5 10.7C0.899997 9.19999 0.799997 7.49999 1.3 5.99999C1.8 4.39999 2.7 3.09999 4.1 2.19999C5.4 1.29999 7 0.899985 8.6 0.999985ZM9.1 13.9C10.4 13.6 11.6 12.9 12.5 11.8C13.3 10.7 13.8 9.39999 13.7 7.99999C13.7 6.39999 13.1 4.79999 12 3.69999C11 2.69999 9.8 2.09999 8.4 1.99999C7.1 1.89999 5.7 2.19999 4.6 2.99999C3.5 3.79999 2.7 4.89999 2.3 6.29999C1.9 7.59999 1.9 8.99999 2.5 10.3C3.1 11.6 4 12.6 5.2 13.3C6.4 14 7.8 14.2 9.1 13.9Z" fill="#388A34"/> +</svg> diff --git a/resources/light/status-unknown.svg b/resources/light/status-unknown.svg new file mode 100644 index 000000000000..25d74d5023cf --- /dev/null +++ b/resources/light/status-unknown.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M7.50001 1C6.21443 1 4.95772 1.38123 3.8888 2.09546C2.81988 2.80969 1.98679 3.82485 1.49482 5.01257C1.00285 6.20029 0.874083 7.50719 1.12489 8.76807C1.37569 10.0289 1.99478 11.1872 2.90382 12.0962C3.81286 13.0052 4.97107 13.6243 6.23194 13.8751C7.49282 14.1259 8.79972 13.9972 9.98744 13.5052C11.1752 13.0133 12.1903 12.1801 12.9045 11.1112C13.6188 10.0423 14 8.78558 14 7.5C14 5.77609 13.3152 4.1228 12.0962 2.90381C10.8772 1.68482 9.22392 1 7.50001 1ZM7.50001 13C6.41221 13 5.34881 12.6775 4.44434 12.0731C3.53987 11.4688 2.83493 10.6097 2.41865 9.60474C2.00237 8.59974 1.89344 7.4939 2.10566 6.427C2.31788 5.36011 2.84172 4.38015 3.61091 3.61096C4.3801 2.84177 5.36012 2.31793 6.42701 2.10571C7.49391 1.89349 8.59975 2.00242 9.60474 2.4187C10.6097 2.83498 11.4687 3.53987 12.0731 4.44434C12.6774 5.34881 13 6.4122 13 7.5C13 8.95869 12.4206 10.3576 11.3891 11.389C10.3577 12.4205 8.9587 13 7.50001 13ZM9.04999 4.57994C8.87722 4.40004 8.6697 4.25723 8.44 4.16002C8.151 4.04431 7.84117 3.98979 7.53003 3.99999C7.22804 3.9945 6.92825 4.05246 6.65002 4.17003C6.41146 4.27028 6.19928 4.42423 6.03003 4.61998C5.86442 4.8001 5.73536 5.01066 5.65002 5.23998C5.57068 5.47292 5.52028 5.7147 5.5 5.95995H6.72998C6.73725 5.74494 6.82673 5.54098 6.97998 5.39C7.05193 5.31511 7.13924 5.25671 7.2359 5.21874C7.33256 5.18076 7.43629 5.16414 7.53998 5.17003C7.62942 5.15581 7.72056 5.15581 7.81 5.17003C7.89216 5.2011 7.96708 5.24877 8.03003 5.31004C8.0995 5.37016 8.15422 5.4454 8.19 5.53001C8.23097 5.62465 8.25141 5.72683 8.25 5.82994C8.25037 6.00265 8.21283 6.17333 8.14001 6.32994C8.06739 6.49283 7.97681 6.6472 7.87 6.79002L7.52002 7.20995C7.40002 7.33995 7.27998 7.47998 7.16998 7.61998C7.06332 7.75933 6.97279 7.91024 6.90002 8.06993C6.83065 8.22732 6.79648 8.39797 6.79999 8.56993V9.22997H8V8.73998C8.00339 8.59331 8.04105 8.44943 8.10999 8.31993C8.19183 8.17576 8.28551 8.03871 8.39001 7.91002L8.75 7.46996C8.88106 7.31855 9.00134 7.15818 9.10999 6.98998C9.22491 6.81846 9.31894 6.63376 9.39001 6.43993C9.46294 6.23444 9.50013 6.01808 9.5 5.80003C9.50178 5.57285 9.46807 5.34675 9.40002 5.12999C9.32451 4.9235 9.20506 4.7358 9.04999 4.57994ZM6.8 9.82996H7.97V11H6.8V9.82996Z" fill="#424242"/> +</svg> diff --git a/resources/light/stop.svg b/resources/light/stop.svg new file mode 100644 index 000000000000..41a99170575a --- /dev/null +++ b/resources/light/stop.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M12.8 2.99999C11.7 1.89999 10.2 1.09999 8.6 0.999985C7 0.899985 5.4 1.29999 4.1 2.19999C2.7 3.09999 1.8 4.39999 1.3 5.99999C0.799997 7.49999 0.899997 9.19999 1.5 10.7C2.1 12.2 3.2 13.4 4.6 14.2C6 15 7.6 15.2 9.2 14.9C10.8 14.6 12.2 13.7 13.2 12.5C14.2 11.2 14.8 9.69999 14.8 8.09999C14.8 6.09999 14.1 4.39999 12.8 2.99999ZM12.5 11.8C11.6 12.9 10.4 13.6 9.1 13.9C7.8 14.2 6.4 14 5.2 13.3C4 12.6 3.1 11.6 2.5 10.3C1.9 8.99999 1.9 7.59999 2.3 6.29999C2.7 4.89999 3.5 3.79999 4.6 2.99999C5.7 2.19999 7.1 1.89999 8.4 1.99999C9.8 2.09999 11 2.69999 12 3.69999C13.1 4.79999 13.7 6.39999 13.7 7.99999C13.8 9.39999 13.3 10.7 12.5 11.8Z" fill="#A1260D"/> +<path d="M6 6H10V10H6V6Z" fill="#A1260D"/> +</svg> diff --git a/resources/light/trusted.svg b/resources/light/trusted.svg new file mode 100644 index 000000000000..82e41b9ff47b --- /dev/null +++ b/resources/light/trusted.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" + xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M7.67 14.72H8.38L10.1 13H12.5L13 12.5V10.08L14.74 8.36004V7.65004L13.03 5.93004V3.49004L12.53 3.00004H10.1L8.38 1.29004H7.67L6 3.00004H3.53L3 3.50004V5.93004L1.31 7.65004V8.36004L3 10.08V12.5L3.53 13H6L7.67 14.72ZM6.16 12H4V9.87004L3.88 9.52004L2.37 8.00004L3.85 6.49004L4 6.14004V4.00004H6.16L6.52 3.86004L8 2.35004L9.54 3.86004L9.89 4.00004H12V6.14004L12.17 6.49004L13.69 8.00004L12.14 9.52004L12 9.87004V12H9.89L9.51 12.15L8 13.66L6.52 12.14L6.16 12ZM6.73003 10.4799H7.44004L11.21 6.71L10.5 6L7.09004 9.41991L5.71 8.03984L5 8.74984L6.73003 10.4799Z" fill="#424242"/> +</svg> diff --git a/resources/light/un-trusted.svg b/resources/light/un-trusted.svg new file mode 100644 index 000000000000..c9be7cd88dda --- /dev/null +++ b/resources/light/un-trusted.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" + xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M7.67 14.72H8.38L10.1 13H12.5L13 12.5V10.08L14.74 8.36004V7.65004L13.03 5.93004V3.49004L12.53 3.00004H10.1L8.38 1.29004H7.67L6 3.00004H3.53L3 3.50004V5.93004L1.31 7.65004V8.36004L3 10.08V12.5L3.53 13H6L7.67 14.72ZM6.16 12H4V9.87004L3.88 9.52004L2.37 8.00004L3.85 6.49004L4 6.14004V4.00004H6.16L6.52 3.86004L8 2.35004L9.54 3.86004L9.89 4.00004H12V6.14004L12.17 6.49004L13.69 8.00004L12.14 9.52004L12 9.87004V12H9.89L9.51 12.15L8 13.66L6.52 12.14L6.16 12ZM7.60288 6.14101C7.54621 6.22606 7.50337 6.32292 7.47465 6.43203C7.44526 6.54079 7.42468 6.64661 7.41299 6.74891L7.40808 6.79186H6.51332L6.51619 6.74069C6.53447 6.41473 6.60464 6.12645 6.72787 5.87687C6.78019 5.76916 6.84468 5.66329 6.92117 5.55926C7.00224 5.45324 7.10082 5.35923 7.21549 5.27864C7.33383 5.19457 7.47018 5.12809 7.62404 5.07882C7.78275 5.02596 7.96334 5 8.16511 5C8.44581 5 8.68831 5.04573 8.89098 5.13921C9.09163 5.22877 9.25661 5.34985 9.38469 5.50292C9.5119 5.65495 9.60489 5.82866 9.66367 6.02356C9.72201 6.21701 9.75116 6.41655 9.75116 6.62201C9.75116 6.81343 9.71872 6.99327 9.65345 7.1611C9.58933 7.32596 9.50992 7.48175 9.41521 7.62841C9.32115 7.77101 9.21774 7.90792 9.10551 8.03835C8.99509 8.16668 8.89371 8.29042 8.80136 8.40959C8.70988 8.52467 8.63315 8.6383 8.57169 8.74952C8.51214 8.85728 8.48332 8.96318 8.48332 9.06777V9.54783H7.59513V9.00932C7.59513 8.84135 7.62627 8.6862 7.68922 8.54455C7.75366 8.4034 7.83362 8.26953 7.92863 8.14386C8.02547 8.01676 8.12847 7.89406 8.2374 7.77606C8.34426 7.66029 8.44365 7.54161 8.53558 7.42003C8.62919 7.30008 8.70548 7.17399 8.76373 7.04293C8.82348 6.9149 8.85397 6.77427 8.85397 6.62201C8.85397 6.50659 8.83811 6.40079 8.80687 6.30424L8.80647 6.303C8.77825 6.20707 8.73432 6.12583 8.67649 6.05974C8.61866 5.99089 8.5471 5.93848 8.46108 5.90241L8.45931 5.90167C8.377 5.86326 8.27937 5.84324 8.16511 5.84324C8.02809 5.84324 7.91599 5.87172 7.82635 5.92596C7.73629 5.9823 7.66193 6.0539 7.60288 6.14101ZM8.5 11H7.6118V10.1118H8.5V11Z" fill="#424242"/> +</svg> diff --git a/resources/report_issue_template.md b/resources/report_issue_template.md new file mode 100644 index 000000000000..a95af90ff7fe --- /dev/null +++ b/resources/report_issue_template.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/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} + +<details> +<summary>User Settings</summary> +<p> + +``` +{3}{4} +``` +</p> +</details> + +<details> +<summary>Installed Extensions</summary> + +|Extension Name|Extension Id|Version| +|---|---|---| +{5} +</details> 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 @@ +<svg width="520" height="220" viewBox="0 0 520 220" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g> + <rect width="520" height="220" fill="var(--vscode-editor-background, #1E1E1E)" /> + <g clip-path="url(#clip0_1003_92716)"> + <g> + <rect width="520" height="39" fill="var(--vscode-editorGroupHeader-tabsBackground, #252526)" /> + <g clip-path="url(#clip1_1003_92716)"> + <g> + <rect width="115.654" height="39.2998" fill="var(--vscode-tab-activeBackground, #1E1E1E)" /> + <rect x="13.4741" y="15.7197" width="88.7052" height="7.85995" rx="3.92998" fill="var(--vscode-tab-unfocusedInactiveForeground, #2D2D2D)" /> + </g> + </g> + </g> + </g> + <g filter="url(#filter0_d_1003_92716)"> + <rect width="425" height="162" transform="translate(48 20)" fill="var(--vscode-quickInput-background, #252526)" /> + <g> + <g> + <rect x="55.5" y="27.5" width="410" height="25" fill="var(--vscode-input-background, #3C3C3C)" /> + <rect x="55.5" y="27.5" width="410" height="25" stroke="var(--vscode-focusBorder, #007FD4)" /> + </g> + </g> + <g> + <g> + <g> + <rect x="60" y="66" width="29" height="6" rx="3" fill="var(--vscode-textLink-foreground, #3794FF)" fill-opacity="0.6" /> + <rect x="93" y="66" width="38" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #454545)" /> + <rect x="135" y="66" width="105" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #454545)" /> + </g> + </g> + <g> + <g> + <rect width="425" height="24" transform="translate(48 82)" fill="var(--vscode-quickInputList-focusBackground, #062F4A)" /> + <path d="M65.2812 99L68.5376 89.8403H67.0269L64.5703 97.2988H64.4624L61.9932 89.8403H60.4443L63.7197 99H65.2812ZM73.8252 97.1528C73.5713 97.7114 73.0063 98.0161 72.1812 98.0161C71.0894 98.0161 70.3848 97.229 70.3403 95.9785V95.915H75.2217V95.4453C75.2217 93.2871 74.0601 91.9731 72.1367 91.9731C70.188 91.9731 68.9502 93.376 68.9502 95.5659C68.9502 97.7686 70.1626 99.1333 72.1431 99.1333C73.7236 99.1333 74.8408 98.3716 75.1392 97.1528H73.8252ZM72.1304 93.084C73.1396 93.084 73.7998 93.814 73.8315 94.9312H70.3403C70.4165 93.8203 71.1211 93.084 72.1304 93.084ZM76.8594 99H78.2241V94.9565C78.2241 93.8457 78.8652 93.1602 79.8745 93.1602C80.8838 93.1602 81.3662 93.7188 81.3662 94.8613V99H82.731V94.5376C82.731 92.8936 81.8804 91.9731 80.3379 91.9731C79.2969 91.9731 78.6113 92.4365 78.2749 93.1982H78.1733V92.1064H76.8594V99ZM90.2783 92.1064H88.8311L87.1172 97.6035H87.0093L85.2891 92.1064H83.8291L86.3237 99H87.79L90.2783 92.1064Z" fill="var(--vscode-list-activeSelectionForeground, #FFFFFF)" /> + <rect x="101" y="91" width="106" height="6" rx="3" fill="var(--vscode-tab-unfocusedInactiveForeground, #FFFFFF)" /> + </g> + <g> + <path d="M64.9067 123.222C66.9507 123.222 68.4551 122.048 68.709 120.264H67.2935C67.0396 121.305 66.1191 121.959 64.9067 121.959C63.2563 121.959 62.228 120.601 62.228 118.423C62.228 116.246 63.2563 114.881 64.9004 114.881C66.1064 114.881 67.0269 115.605 67.2935 116.741H68.709C68.4805 114.907 66.9189 113.618 64.9004 113.618C62.355 113.618 60.7744 115.459 60.7744 118.423C60.7744 121.381 62.3613 123.222 64.9067 123.222ZM73.3428 123.133C75.3613 123.133 76.5991 121.781 76.5991 119.553C76.5991 117.325 75.355 115.973 73.3428 115.973C71.3242 115.973 70.0801 117.332 70.0801 119.553C70.0801 121.781 71.3179 123.133 73.3428 123.133ZM73.3428 121.972C72.1558 121.972 71.4893 121.089 71.4893 119.553C71.4893 118.017 72.1558 117.128 73.3428 117.128C74.5234 117.128 75.1963 118.017 75.1963 119.553C75.1963 121.083 74.5234 121.972 73.3428 121.972ZM78.2432 123H79.6079V118.957C79.6079 117.846 80.249 117.16 81.2583 117.16C82.2676 117.16 82.75 117.719 82.75 118.861V123H84.1147V118.538C84.1147 116.894 83.2642 115.973 81.7217 115.973C80.6807 115.973 79.9951 116.437 79.6587 117.198H79.5571V116.106H78.2432V123ZM88.5645 123.114C89.5166 123.114 90.3228 122.664 90.7354 121.908H90.8433V123H92.1509V113.402H90.7861V117.198H90.6846C90.3101 116.443 89.5103 115.986 88.5645 115.986C86.8188 115.986 85.6953 117.376 85.6953 119.547C85.6953 121.73 86.8062 123.114 88.5645 123.114ZM88.9517 117.154C90.0942 117.154 90.8115 118.081 90.8115 119.553C90.8115 121.039 90.1006 121.946 88.9517 121.946C87.7964 121.946 87.1045 121.051 87.1045 119.553C87.1045 118.062 87.8027 117.154 88.9517 117.154ZM96.1118 123.114C97.0195 123.114 97.7749 122.721 98.1875 122.022H98.2954V123H99.6094V118.284C99.6094 116.836 98.6318 115.973 96.8989 115.973C95.3311 115.973 94.2139 116.729 94.0742 117.89H95.3945C95.5469 117.389 96.0737 117.103 96.8354 117.103C97.7686 117.103 98.251 117.528 98.251 118.284V118.887L96.3784 119.001C94.7344 119.103 93.8076 119.82 93.8076 121.058C93.8076 122.314 94.7788 123.114 96.1118 123.114ZM96.4609 122.016C95.7183 122.016 95.1787 121.642 95.1787 121C95.1787 120.372 95.6104 120.036 96.5625 119.972L98.251 119.858V120.455C98.251 121.343 97.4893 122.016 96.4609 122.016Z" fill="var(--vscode-editor-foreground, #CCCCCC)" /> + <rect x="111" y="115" width="154" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #454545)" /> + </g> + </g> + <g> + <g> + <rect x="60" y="140" width="41" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #454545)" /> + <rect x="105" y="140" width="69" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #454545)" /> + <rect x="178" y="140" width="26" height="6" rx="3" fill="var(--vscode-textLink-foreground, #3794FF)" fill-opacity="0.6" /> + </g> + </g> + <g> + <g> + <rect x="60" y="162" width="26" height="6" rx="3" fill="var(--vscode-textLink-foreground, #3794FF)" fill-opacity="0.6" /> + <rect x="90" y="162" width="164" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #454545)" /> + </g> + </g> + </g> + </g> + </g> + <defs> + <filter id="filter0_d_1003_92716" x="32" y="6" width="457" height="194" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> + <feFlood flood-opacity="0" result="BackgroundImageFix" /> + <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> + <feOffset dy="2" /> + <feGaussianBlur stdDeviation="8" /> + <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.36 0" /> + <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1003_92716" /> + <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1003_92716" result="shape" /> + </filter> + <clipPath id="clip0_1003_92716"> + <rect width="520" height="220" fill="#FFFFFF" /> + </clipPath> + <clipPath id="clip1_1003_92716"> + <rect width="115.654" height="39.2998" fill="#FFFFFF" /> + </clipPath> + </defs> +</svg> \ 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 @@ +<svg width="554" height="337" viewBox="0 0 554 337" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g opacity="0.4"> + <g filter="url(#filter0_d_30214831)"> + <rect width="519" height="134" transform="translate(12 189)" fill="var(--vscode-editorGroupHeader-tabsBackground, #292929)" /> + <rect x="25" y="221" width="4" height="90" rx="2" fill="var(--vscode-focusBorder, #007FD4)" /> + <path d="M40.7867 225.973L39.9867 226.4V238.4L40.7867 238.827L49.8 232.853V232L40.7867 225.973ZM41 237.493V227.36L48.6267 232.427L41 237.493Z" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <path d="M63 234.08L67.32 229.707L67.96 230.347L63.2667 234.987H62.68L57.9867 230.347L58.6267 229.707L63 234.08Z" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <rect x="75.5" y="221.5" width="440" height="59" fill="var(--vscode-notificationCenterHeader-background, #303031)" stroke="#424750" /> + <rect x="374.5" y="205.5" width="152" height="28" fill="var(--vscode-editor-background, #292929)" stroke="#424750" /> + <path d="M408.76 213.013L408.013 213.44V225.44L408.76 225.813L417.773 219.84V218.987L408.76 213.013ZM409.027 224.48V214.347L416.6 219.413L409.027 224.48ZM419.16 220H419.853L422.36 222.507L421.667 223.2L420.013 221.547V226.987H419V221.547L417.347 223.2L416.653 222.507L419.16 220Z" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <path d="M432.813 213.013L432.013 213.44V225.44L432.813 225.813L441.827 219.84V218.987L432.813 213.013ZM433.027 224.48V214.347L440.6 219.413L433.027 224.48ZM443.853 226.987H443.16L440.653 224.48L441.347 223.787L443 225.44V220H444.013V225.44L445.667 223.787L446.36 224.533L443.853 226.987Z" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <path d="M483 220C483 220.284 482.893 220.533 482.68 220.747C482.502 220.924 482.271 221.013 481.987 221.013C481.702 221.013 481.471 220.924 481.293 220.747C481.116 220.533 481.027 220.284 481.027 220C481.027 219.716 481.116 219.484 481.293 219.307C481.471 219.093 481.702 218.987 481.987 218.987C482.271 218.987 482.502 219.093 482.68 219.307C482.893 219.484 483 219.716 483 220ZM488.013 220C488.013 220.284 487.907 220.533 487.693 220.747C487.516 220.924 487.284 221.013 487 221.013C486.716 221.013 486.467 220.924 486.253 220.747C486.076 220.533 485.987 220.284 485.987 220C485.987 219.716 486.076 219.484 486.253 219.307C486.467 219.093 486.716 218.987 487 218.987C487.284 218.987 487.516 219.093 487.693 219.307C487.907 219.484 488.013 219.716 488.013 220ZM493.027 220C493.027 220.284 492.92 220.533 492.707 220.747C492.529 220.924 492.298 221.013 492.013 221.013C491.729 221.013 491.48 220.924 491.267 220.747C491.089 220.533 491 220.284 491 220C491 219.716 491.089 219.484 491.267 219.307C491.48 219.093 491.729 218.987 492.013 218.987C492.298 218.987 492.529 219.093 492.707 219.307C492.92 219.484 493.027 219.716 493.027 220Z" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <path d="M513.027 214.987H516.013V216H515V225.013L513.987 225.973H507L505.987 225.013V216H505.027V214.987H508.013V213.973C508.013 213.724 508.102 213.511 508.28 213.333C508.493 213.12 508.742 213.013 509.027 213.013H512.013C512.298 213.013 512.529 213.12 512.707 213.333C512.92 213.511 513.027 213.724 513.027 213.973V214.987ZM512.013 213.973H509.027V214.987H512.013V213.973ZM507 225.013H513.987V216H507V225.013ZM509.027 217.013H508.013V224H509.027V217.013ZM509.987 217.013H511V224H509.987V217.013ZM512.013 217.013H513.027V224H512.013V217.013Z" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <path d="M389.027 221.973V221.013H397.987V221.973H389.027ZM393.027 217.973H397.987V218.987H393.027V217.973ZM397.987 214.987V216H389.027V214.987H397.987ZM389.027 224V225.013H397.987V224H389.027ZM384.013 214.773L384.76 214.4L390.147 217.973V218.773L384.76 222.4L384.013 221.973V214.773ZM385.027 215.733V221.013L388.973 218.4L385.027 215.733Z" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <path d="M469.027 213.013H457.987L457.027 213.973V225.013L457.987 225.973H469.027L469.987 225.013V213.973L469.027 213.013ZM469.027 225.013H457.987V220H469.027V225.013ZM469.027 218.987H457.987V213.973H469.027V218.987Z" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <path d="M88.6582 241.309V246H87.6545V239.455H88.6582V240.327H88.8764C89.0145 240.022 89.1964 239.782 89.4218 239.607C89.6473 239.433 89.8836 239.345 90.1309 239.345C90.4145 239.345 90.6509 239.433 90.84 239.607C91.0364 239.775 91.1709 240.015 91.2436 240.327H91.4836C91.6218 240.022 91.8073 239.782 92.04 239.607C92.28 239.433 92.5345 239.345 92.8036 239.345C93.1673 239.345 93.4545 239.495 93.6655 239.793C93.8764 240.084 93.9818 240.48 93.9818 240.982V246H92.9782V241.156C92.9782 240.829 92.9345 240.582 92.8473 240.415C92.76 240.247 92.6327 240.164 92.4655 240.164C92.3273 240.164 92.1527 240.265 91.9418 240.469C91.7382 240.673 91.5309 240.953 91.32 241.309V246H90.3164V241.156C90.3164 240.829 90.2691 240.582 90.1745 240.415C90.0873 240.247 89.96 240.164 89.7927 240.164C89.6473 240.164 89.4764 240.262 89.28 240.458C89.0909 240.655 88.8836 240.938 88.6582 241.309ZM98.5461 246.109C97.9934 246.109 97.4588 246.029 96.9424 245.869C96.4261 245.702 95.9497 245.447 95.5134 245.105L96.0043 244.244C96.3606 244.535 96.757 244.756 97.1934 244.909C97.637 245.055 98.0879 245.127 98.5461 245.127C98.9243 245.127 99.2843 245.069 99.6261 244.953C99.9752 244.836 100.15 244.589 100.15 244.211C100.15 243.898 100.008 243.676 99.7243 243.545C99.4406 243.415 98.9206 243.302 98.1643 243.207C97.3643 243.113 96.757 242.927 96.3424 242.651C95.9352 242.367 95.7315 241.924 95.7315 241.32C95.7315 240.665 95.9788 240.175 96.4734 239.847C96.9679 239.513 97.5424 239.345 98.197 239.345C98.7134 239.345 99.2079 239.433 99.6806 239.607C100.161 239.782 100.604 240.029 101.012 240.349L100.521 241.211C100.186 240.942 99.8224 240.727 99.4297 240.567C99.037 240.407 98.6261 240.327 98.197 240.327C97.8479 240.327 97.5279 240.393 97.237 240.524C96.9534 240.655 96.8115 240.895 96.8115 241.244C96.8115 241.542 96.9424 241.76 97.2043 241.898C97.4734 242.029 97.9861 242.142 98.7424 242.236C99.5715 242.331 100.193 242.535 100.608 242.847C101.022 243.153 101.23 243.582 101.23 244.135C101.23 244.818 100.953 245.32 100.401 245.64C99.8479 245.953 99.2297 246.109 98.5461 246.109ZM109.372 246.524C109.372 247.113 109.154 247.553 108.718 247.844C108.289 248.142 107.761 248.291 107.136 248.291H105.085C104.43 248.291 103.889 248.16 103.459 247.898C103.038 247.636 102.827 247.233 102.827 246.687C102.827 246.338 102.918 246.062 103.099 245.858C103.289 245.647 103.561 245.527 103.918 245.498V245.269C103.663 245.225 103.452 245.12 103.285 244.953C103.125 244.785 103.045 244.527 103.045 244.178C103.045 243.865 103.143 243.585 103.339 243.338C103.536 243.084 103.867 242.938 104.332 242.902V242.695C103.976 242.578 103.736 242.404 103.612 242.171C103.489 241.931 103.427 241.702 103.427 241.484V241.189C103.427 240.687 103.641 240.273 104.07 239.945C104.499 239.618 105.103 239.455 105.881 239.455H109.099V240.436L107.409 240.185V240.382C107.699 240.462 107.918 240.615 108.063 240.84C108.216 241.065 108.292 241.291 108.292 241.516V241.8C108.292 242.302 108.099 242.709 107.714 243.022C107.329 243.327 106.703 243.48 105.838 243.48H104.518C104.321 243.48 104.172 243.531 104.07 243.633C103.969 243.727 103.918 243.891 103.918 244.124C103.918 244.349 103.987 244.52 104.125 244.636C104.27 244.745 104.485 244.8 104.769 244.8H107.245C107.856 244.8 108.361 244.935 108.761 245.204C109.169 245.473 109.372 245.913 109.372 246.524ZM105.881 242.651C106.325 242.651 106.663 242.571 106.896 242.411C107.129 242.251 107.245 242 107.245 241.658V241.309C107.245 240.967 107.129 240.72 106.896 240.567C106.663 240.407 106.325 240.327 105.881 240.327C105.438 240.327 105.099 240.407 104.867 240.567C104.634 240.72 104.518 240.967 104.518 241.309V241.658C104.518 242 104.634 242.251 104.867 242.411C105.099 242.571 105.438 242.651 105.881 242.651ZM105.063 247.418H107.278C107.605 247.418 107.87 247.356 108.074 247.233C108.278 247.109 108.379 246.895 108.379 246.589C108.379 246.298 108.285 246.091 108.096 245.967C107.914 245.844 107.678 245.782 107.387 245.782H103.918V246.524C103.918 246.822 104.016 247.044 104.212 247.189C104.409 247.342 104.692 247.418 105.063 247.418ZM134.502 259.309V264H133.498V257.455H134.502V258.327H134.72C134.858 258.022 135.04 257.782 135.266 257.607C135.491 257.433 135.727 257.345 135.975 257.345C136.258 257.345 136.495 257.433 136.684 257.607C136.88 257.775 137.015 258.015 137.087 258.327H137.327C137.466 258.022 137.651 257.782 137.884 257.607C138.124 257.433 138.378 257.345 138.647 257.345C139.011 257.345 139.298 257.495 139.509 257.793C139.72 258.084 139.826 258.48 139.826 258.982V264H138.822V259.156C138.822 258.829 138.778 258.582 138.691 258.415C138.604 258.247 138.476 258.164 138.309 258.164C138.171 258.164 137.996 258.265 137.786 258.469C137.582 258.673 137.375 258.953 137.164 259.309V264H136.16V259.156C136.16 258.829 136.113 258.582 136.018 258.415C135.931 258.247 135.804 258.164 135.636 258.164C135.491 258.164 135.32 258.262 135.124 258.458C134.935 258.655 134.727 258.938 134.502 259.309ZM144.39 264.109C143.837 264.109 143.303 264.029 142.786 263.869C142.27 263.702 141.793 263.447 141.357 263.105L141.848 262.244C142.204 262.535 142.601 262.756 143.037 262.909C143.481 263.055 143.932 263.127 144.39 263.127C144.768 263.127 145.128 263.069 145.47 262.953C145.819 262.836 145.993 262.589 145.993 262.211C145.993 261.898 145.852 261.676 145.568 261.545C145.284 261.415 144.764 261.302 144.008 261.207C143.208 261.113 142.601 260.927 142.186 260.651C141.779 260.367 141.575 259.924 141.575 259.32C141.575 258.665 141.823 258.175 142.317 257.847C142.812 257.513 143.386 257.345 144.041 257.345C144.557 257.345 145.052 257.433 145.524 257.607C146.004 257.782 146.448 258.029 146.855 258.349L146.364 259.211C146.03 258.942 145.666 258.727 145.273 258.567C144.881 258.407 144.47 258.327 144.041 258.327C143.692 258.327 143.372 258.393 143.081 258.524C142.797 258.655 142.655 258.895 142.655 259.244C142.655 259.542 142.786 259.76 143.048 259.898C143.317 260.029 143.83 260.142 144.586 260.236C145.415 260.331 146.037 260.535 146.452 260.847C146.866 261.153 147.073 261.582 147.073 262.135C147.073 262.818 146.797 263.32 146.244 263.64C145.692 263.953 145.073 264.109 144.39 264.109ZM155.216 264.524C155.216 265.113 154.998 265.553 154.561 265.844C154.132 266.142 153.605 266.291 152.98 266.291H150.929C150.274 266.291 149.732 266.16 149.303 265.898C148.881 265.636 148.67 265.233 148.67 264.687C148.67 264.338 148.761 264.062 148.943 263.858C149.132 263.647 149.405 263.527 149.761 263.498V263.269C149.507 263.225 149.296 263.12 149.129 262.953C148.969 262.785 148.889 262.527 148.889 262.178C148.889 261.865 148.987 261.585 149.183 261.338C149.38 261.084 149.71 260.938 150.176 260.902V260.695C149.82 260.578 149.58 260.404 149.456 260.171C149.332 259.931 149.27 259.702 149.27 259.484V259.189C149.27 258.687 149.485 258.273 149.914 257.945C150.343 257.618 150.947 257.455 151.725 257.455H154.943V258.436L153.252 258.185V258.382C153.543 258.462 153.761 258.615 153.907 258.84C154.06 259.065 154.136 259.291 154.136 259.516V259.8C154.136 260.302 153.943 260.709 153.558 261.022C153.172 261.327 152.547 261.48 151.681 261.48H150.361C150.165 261.48 150.016 261.531 149.914 261.633C149.812 261.727 149.761 261.891 149.761 262.124C149.761 262.349 149.83 262.52 149.969 262.636C150.114 262.745 150.329 262.8 150.612 262.8H153.089C153.7 262.8 154.205 262.935 154.605 263.204C155.012 263.473 155.216 263.913 155.216 264.524ZM151.725 260.651C152.169 260.651 152.507 260.571 152.74 260.411C152.972 260.251 153.089 260 153.089 259.658V259.309C153.089 258.967 152.972 258.72 152.74 258.567C152.507 258.407 152.169 258.327 151.725 258.327C151.281 258.327 150.943 258.407 150.71 258.567C150.478 258.72 150.361 258.967 150.361 259.309V259.658C150.361 260 150.478 260.251 150.71 260.411C150.943 260.571 151.281 260.651 151.725 260.651ZM150.907 265.418H153.121C153.449 265.418 153.714 265.356 153.918 265.233C154.121 265.109 154.223 264.895 154.223 264.589C154.223 264.298 154.129 264.091 153.94 263.967C153.758 263.844 153.521 263.782 153.23 263.782H149.761V264.524C149.761 264.822 149.86 265.044 150.056 265.189C150.252 265.342 150.536 265.418 150.907 265.418Z" fill="#9CDCFE" /> + <path d="M118.326 241.2V240.109H124.435V241.2H118.326ZM118.326 244.255V243.164H124.435V244.255H118.326ZM131.203 266.291C130.061 266.022 129.138 265.4 128.432 264.425C127.734 263.451 127.385 262.218 127.385 260.727C127.385 259.236 127.734 258.004 128.432 257.029C129.138 256.055 130.061 255.433 131.203 255.164L131.312 256.015C130.338 256.284 129.621 256.847 129.163 257.705C128.705 258.556 128.476 259.564 128.476 260.727C128.476 261.891 128.705 262.902 129.163 263.76C129.621 264.611 130.338 265.171 131.312 265.44L131.203 266.291ZM157.402 266.291L157.293 265.44C158.267 265.171 158.984 264.607 159.442 263.749C159.9 262.884 160.129 261.876 160.129 260.727C160.129 259.578 159.9 258.575 159.442 257.716C158.984 256.851 158.267 256.284 157.293 256.015L157.402 255.164C158.544 255.433 159.464 256.058 160.162 257.04C160.867 258.015 161.22 259.244 161.22 260.727C161.22 262.211 160.867 263.44 160.162 264.415C159.464 265.396 158.544 266.022 157.402 266.291Z" fill="#D4D4D4" /> + <path d="M137.753 241.636L137.426 237.273H139.171L138.844 241.636H137.753ZM134.48 241.636L134.153 237.273H135.898L135.571 241.636H134.48ZM145.59 246L144.303 240.796L143.015 246H141.924L140.746 237.273H141.826L142.612 244.396L143.757 239.455H144.903L146.048 244.396L146.833 237.273H147.859L146.681 246H145.59ZM154.692 244.56C154.496 245.105 154.198 245.502 153.798 245.749C153.398 245.989 152.783 246.109 151.954 246.109C151.089 246.109 150.416 245.902 149.936 245.487C149.456 245.065 149.216 244.444 149.216 243.622V241.865C149.216 241.022 149.467 240.393 149.969 239.978C150.478 239.556 151.14 239.345 151.954 239.345C152.776 239.345 153.434 239.56 153.929 239.989C154.423 240.411 154.67 241.062 154.67 241.942V242.945H150.307V243.633C150.307 244.178 150.441 244.575 150.71 244.822C150.987 245.062 151.398 245.182 151.943 245.182C152.467 245.182 152.849 245.109 153.089 244.964C153.329 244.818 153.51 244.582 153.634 244.255L154.692 244.56ZM150.307 241.865V242.127H153.58V241.964C153.58 241.375 153.441 240.945 153.165 240.676C152.896 240.407 152.489 240.273 151.943 240.273C151.398 240.273 150.987 240.404 150.71 240.665C150.441 240.92 150.307 241.32 150.307 241.865ZM156.857 246V245.018H159.038V238.255H156.857V237.273H160.129V245.018H162.311V246H156.857ZM167.377 246.109C166.424 246.109 165.704 245.902 165.217 245.487C164.737 245.065 164.497 244.447 164.497 243.633V241.865C164.497 241.036 164.737 240.411 165.217 239.989C165.704 239.56 166.421 239.345 167.366 239.345C168.21 239.345 168.857 239.538 169.308 239.924C169.766 240.302 170.021 240.858 170.072 241.593L169.003 241.658C168.974 241.207 168.824 240.873 168.555 240.655C168.286 240.436 167.886 240.327 167.355 240.327C166.759 240.327 166.315 240.455 166.024 240.709C165.734 240.956 165.588 241.342 165.588 241.865V243.633C165.588 244.135 165.734 244.509 166.024 244.756C166.323 245.004 166.774 245.127 167.377 245.127C167.901 245.127 168.297 245.036 168.566 244.855C168.835 244.665 168.981 244.385 169.003 244.015L170.072 244.08C170.021 244.749 169.77 245.255 169.319 245.596C168.868 245.938 168.221 246.109 167.377 246.109ZM174.865 246.109C173.985 246.109 173.309 245.898 172.836 245.476C172.371 245.047 172.138 244.433 172.138 243.633V241.865C172.138 241.058 172.374 240.436 172.847 240C173.32 239.564 173.996 239.345 174.876 239.345C175.749 239.345 176.418 239.564 176.883 240C177.356 240.436 177.592 241.058 177.592 241.865V243.633C177.592 244.433 177.356 245.047 176.883 245.476C176.418 245.898 175.745 246.109 174.865 246.109ZM174.865 245.127C175.418 245.127 175.829 245.004 176.098 244.756C176.367 244.509 176.501 244.135 176.501 243.633V241.865C176.501 241.342 176.367 240.956 176.098 240.709C175.829 240.455 175.418 240.327 174.865 240.327C174.312 240.327 173.901 240.455 173.632 240.709C173.363 240.956 173.229 241.342 173.229 241.865V243.633C173.229 244.135 173.363 244.509 173.632 244.756C173.901 245.004 174.312 245.127 174.865 245.127ZM180.346 241.309V246H179.342V239.455H180.346V240.327H180.564C180.702 240.022 180.884 239.782 181.109 239.607C181.335 239.433 181.571 239.345 181.818 239.345C182.102 239.345 182.338 239.433 182.527 239.607C182.724 239.775 182.858 240.015 182.931 240.327H183.171C183.309 240.022 183.495 239.782 183.728 239.607C183.968 239.433 184.222 239.345 184.491 239.345C184.855 239.345 185.142 239.495 185.353 239.793C185.564 240.084 185.669 240.48 185.669 240.982V246H184.666V241.156C184.666 240.829 184.622 240.582 184.535 240.415C184.447 240.247 184.32 240.164 184.153 240.164C184.015 240.164 183.84 240.265 183.629 240.469C183.426 240.673 183.218 240.953 183.008 241.309V246H182.004V241.156C182.004 240.829 181.957 240.582 181.862 240.415C181.775 240.247 181.647 240.164 181.48 240.164C181.335 240.164 181.164 240.262 180.967 240.458C180.778 240.655 180.571 240.938 180.346 241.309ZM192.895 244.56C192.699 245.105 192.401 245.502 192.001 245.749C191.601 245.989 190.986 246.109 190.157 246.109C189.292 246.109 188.619 245.902 188.139 245.487C187.659 245.065 187.419 244.444 187.419 243.622V241.865C187.419 241.022 187.67 240.393 188.172 239.978C188.681 239.556 189.343 239.345 190.157 239.345C190.979 239.345 191.637 239.56 192.132 239.989C192.626 240.411 192.874 241.062 192.874 241.942V242.945H188.51V243.633C188.51 244.178 188.644 244.575 188.914 244.822C189.19 245.062 189.601 245.182 190.146 245.182C190.67 245.182 191.052 245.109 191.292 244.964C191.532 244.818 191.714 244.582 191.837 244.255L192.895 244.56ZM188.51 241.865V242.127H191.783V241.964C191.783 241.375 191.644 240.945 191.368 240.676C191.099 240.407 190.692 240.273 190.146 240.273C189.601 240.273 189.19 240.404 188.914 240.665C188.644 240.92 188.51 241.32 188.51 241.865ZM206.508 246.098C205.788 246.098 205.228 245.92 204.828 245.564C204.428 245.207 204.228 244.662 204.228 243.927V240.436H202.373V239.455H204.228V237.273H205.318V239.455H208.155V240.436H205.318V243.829C205.318 244.251 205.424 244.564 205.635 244.767C205.846 244.971 206.158 245.073 206.573 245.073C206.849 245.073 207.108 245.055 207.348 245.018C207.588 244.975 207.817 244.92 208.035 244.855L208.253 245.847C208.006 245.92 207.74 245.978 207.457 246.022C207.18 246.073 206.864 246.098 206.508 246.098ZM213.068 246.109C212.188 246.109 211.512 245.898 211.039 245.476C210.574 245.047 210.341 244.433 210.341 243.633V241.865C210.341 241.058 210.577 240.436 211.05 240C211.523 239.564 212.199 239.345 213.079 239.345C213.952 239.345 214.621 239.564 215.086 240C215.559 240.436 215.795 241.058 215.795 241.865V243.633C215.795 244.433 215.559 245.047 215.086 245.476C214.621 245.898 213.948 246.109 213.068 246.109ZM213.068 245.127C213.621 245.127 214.032 245.004 214.301 244.756C214.57 244.509 214.705 244.135 214.705 243.633V241.865C214.705 241.342 214.57 240.956 214.301 240.709C214.032 240.455 213.621 240.327 213.068 240.327C212.515 240.327 212.105 240.455 211.835 240.709C211.566 240.956 211.432 241.342 211.432 241.865V243.633C211.432 244.135 211.566 244.509 211.835 244.756C212.105 245.004 212.515 245.127 213.068 245.127ZM229.517 246L226.604 238.647V246H225.622V237.273H227.117L230.095 244.789V237.273H231.077V246H229.517ZM235.99 246.109C235.11 246.109 234.434 245.898 233.961 245.476C233.496 245.047 233.263 244.433 233.263 243.633V241.865C233.263 241.058 233.499 240.436 233.972 240C234.445 239.564 235.121 239.345 236.001 239.345C236.874 239.345 237.543 239.564 238.008 240C238.481 240.436 238.717 241.058 238.717 241.865V243.633C238.717 244.433 238.481 245.047 238.008 245.476C237.543 245.898 236.87 246.109 235.99 246.109ZM235.99 245.127C236.543 245.127 236.954 245.004 237.223 244.756C237.492 244.509 237.626 244.135 237.626 243.633V241.865C237.626 241.342 237.492 240.956 237.223 240.709C236.954 240.455 236.543 240.327 235.99 240.327C235.437 240.327 235.026 240.455 234.757 240.709C234.488 240.956 234.354 241.342 234.354 241.865V243.633C234.354 244.135 234.488 244.509 234.757 244.756C235.026 245.004 235.437 245.127 235.99 245.127ZM244.711 246.098C243.991 246.098 243.431 245.92 243.031 245.564C242.631 245.207 242.431 244.662 242.431 243.927V240.436H240.576V239.455H242.431V237.273H243.522V239.455H246.358V240.436H243.522V243.829C243.522 244.251 243.627 244.564 243.838 244.767C244.049 244.971 244.362 245.073 244.776 245.073C245.052 245.073 245.311 245.055 245.551 245.018C245.791 244.975 246.02 244.92 246.238 244.855L246.456 245.847C246.209 245.92 245.943 245.978 245.66 246.022C245.383 246.073 245.067 246.098 244.711 246.098ZM254.02 244.56C253.824 245.105 253.526 245.502 253.126 245.749C252.726 245.989 252.111 246.109 251.282 246.109C250.417 246.109 249.744 245.902 249.264 245.487C248.784 245.065 248.544 244.444 248.544 243.622V241.865C248.544 241.022 248.795 240.393 249.297 239.978C249.806 239.556 250.468 239.345 251.282 239.345C252.104 239.345 252.762 239.56 253.257 239.989C253.751 240.411 253.999 241.062 253.999 241.942V242.945H249.635V243.633C249.635 244.178 249.769 244.575 250.039 244.822C250.315 245.062 250.726 245.182 251.271 245.182C251.795 245.182 252.177 245.109 252.417 244.964C252.657 244.818 252.839 244.582 252.962 244.255L254.02 244.56ZM249.635 241.865V242.127H252.908V241.964C252.908 241.375 252.769 240.945 252.493 240.676C252.224 240.407 251.817 240.273 251.271 240.273C250.726 240.273 250.315 240.404 250.039 240.665C249.769 240.92 249.635 241.32 249.635 241.865ZM258.825 246.109C258.388 246.109 257.937 246.044 257.472 245.913C257.006 245.782 256.577 245.593 256.185 245.345V237.273H257.276V240.545H257.603C257.792 240.167 258.054 239.873 258.388 239.662C258.723 239.451 259.097 239.345 259.512 239.345C260.188 239.345 260.712 239.564 261.083 240C261.454 240.436 261.639 241.047 261.639 241.833V243.611C261.639 244.404 261.392 245.018 260.897 245.455C260.403 245.891 259.712 246.109 258.825 246.109ZM258.814 245.149C259.403 245.149 259.839 245.022 260.123 244.767C260.406 244.505 260.548 244.113 260.548 243.589V241.844C260.548 241.349 260.439 240.982 260.221 240.742C260.003 240.502 259.676 240.382 259.239 240.382C258.883 240.382 258.545 240.476 258.225 240.665C257.905 240.855 257.588 241.142 257.276 241.527V244.735C257.508 244.873 257.748 244.978 257.996 245.051C258.25 245.116 258.523 245.149 258.814 245.149ZM266.553 246.109C265.673 246.109 264.996 245.898 264.523 245.476C264.058 245.047 263.825 244.433 263.825 243.633V241.865C263.825 241.058 264.062 240.436 264.534 240C265.007 239.564 265.683 239.345 266.563 239.345C267.436 239.345 268.105 239.564 268.571 240C269.043 240.436 269.28 241.058 269.28 241.865V243.633C269.28 244.433 269.043 245.047 268.571 245.476C268.105 245.898 267.433 246.109 266.553 246.109ZM266.553 245.127C267.105 245.127 267.516 245.004 267.785 244.756C268.054 244.509 268.189 244.135 268.189 243.633V241.865C268.189 241.342 268.054 240.956 267.785 240.709C267.516 240.455 267.105 240.327 266.553 240.327C266 240.327 265.589 240.455 265.32 240.709C265.051 240.956 264.916 241.342 264.916 241.865V243.633C264.916 244.135 265.051 244.509 265.32 244.756C265.589 245.004 266 245.127 266.553 245.127ZM274.193 246.109C273.313 246.109 272.637 245.898 272.164 245.476C271.699 245.047 271.466 244.433 271.466 243.633V241.865C271.466 241.058 271.702 240.436 272.175 240C272.648 239.564 273.324 239.345 274.204 239.345C275.077 239.345 275.746 239.564 276.211 240C276.684 240.436 276.92 241.058 276.92 241.865V243.633C276.92 244.433 276.684 245.047 276.211 245.476C275.746 245.898 275.073 246.109 274.193 246.109ZM274.193 245.127C274.746 245.127 275.157 245.004 275.426 244.756C275.695 244.509 275.83 244.135 275.83 243.633V241.865C275.83 241.342 275.695 240.956 275.426 240.709C275.157 240.455 274.746 240.327 274.193 240.327C273.64 240.327 273.23 240.455 272.96 240.709C272.691 240.956 272.557 241.342 272.557 241.865V243.633C272.557 244.135 272.691 244.509 272.96 244.756C273.23 245.004 273.64 245.127 274.193 245.127ZM280.743 243.022V246H279.652V237.273H280.743V242.105H281.299L283.394 239.455H284.725L282.161 242.498L284.692 246H283.448L281.299 243.022H280.743ZM289.562 246.109C289.009 246.109 288.474 246.029 287.958 245.869C287.442 245.702 286.965 245.447 286.529 245.105L287.02 244.244C287.376 244.535 287.773 244.756 288.209 244.909C288.653 245.055 289.104 245.127 289.562 245.127C289.94 245.127 290.3 245.069 290.642 244.953C290.991 244.836 291.165 244.589 291.165 244.211C291.165 243.898 291.024 243.676 290.74 243.545C290.456 243.415 289.936 243.302 289.18 243.207C288.38 243.113 287.773 242.927 287.358 242.651C286.951 242.367 286.747 241.924 286.747 241.32C286.747 240.665 286.994 240.175 287.489 239.847C287.984 239.513 288.558 239.345 289.213 239.345C289.729 239.345 290.224 239.433 290.696 239.607C291.176 239.782 291.62 240.029 292.027 240.349L291.536 241.211C291.202 240.942 290.838 240.727 290.445 240.567C290.053 240.407 289.642 240.327 289.213 240.327C288.864 240.327 288.544 240.393 288.253 240.524C287.969 240.655 287.827 240.895 287.827 241.244C287.827 241.542 287.958 241.76 288.22 241.898C288.489 242.029 289.002 242.142 289.758 242.236C290.587 242.331 291.209 242.535 291.624 242.847C292.038 243.153 292.245 243.582 292.245 244.135C292.245 244.818 291.969 245.32 291.416 245.64C290.864 245.953 290.245 246.109 289.562 246.109ZM302.028 246V245.018H304.21V240.436H302.028V239.455H305.301V245.018H307.483V246H302.028ZM304.756 238.255C304.479 238.255 304.254 238.211 304.079 238.124C303.912 238.036 303.828 237.873 303.828 237.633V237.556C303.828 237.309 303.912 237.142 304.079 237.055C304.254 236.96 304.479 236.913 304.756 236.913C305.032 236.913 305.254 236.96 305.421 237.055C305.596 237.142 305.683 237.309 305.683 237.556V237.633C305.683 237.873 305.596 238.036 305.421 238.124C305.247 238.211 305.025 238.255 304.756 238.255ZM309.669 246V239.455H310.76V240.545H311.087C311.247 240.175 311.498 239.884 311.84 239.673C312.182 239.455 312.571 239.345 313.007 239.345C313.662 239.345 314.178 239.545 314.556 239.945C314.934 240.345 315.124 240.891 315.124 241.582V246H314.033V241.8C314.033 241.342 313.92 240.993 313.694 240.753C313.476 240.505 313.16 240.382 312.745 240.382C312.418 240.382 312.084 240.48 311.742 240.676C311.407 240.873 311.08 241.156 310.76 241.527V246H309.669ZM328.256 246H327.11L324.296 237.273H325.474L327.699 244.615L329.925 237.273H331.07L328.256 246ZM335.46 246.065C334.805 246.065 334.195 245.96 333.627 245.749C333.06 245.538 332.551 245.153 332.1 244.593L332.885 243.851C333.191 244.265 333.555 244.575 333.976 244.778C334.405 244.982 334.911 245.084 335.493 245.084C336.031 245.084 336.464 244.96 336.791 244.713C337.125 244.458 337.293 244.116 337.293 243.687C337.293 243.273 337.151 242.956 336.867 242.738C336.584 242.513 336.024 242.276 335.187 242.029C334.125 241.724 333.402 241.393 333.016 241.036C332.631 240.68 332.438 240.175 332.438 239.52C332.438 238.764 332.689 238.193 333.191 237.807C333.7 237.422 334.315 237.229 335.035 237.229C335.645 237.229 336.224 237.338 336.769 237.556C337.322 237.767 337.809 238.145 338.231 238.691L337.478 239.444C337.187 239.029 336.824 238.72 336.387 238.516C335.951 238.313 335.507 238.211 335.056 238.211C334.598 238.211 334.231 238.305 333.955 238.495C333.678 238.676 333.54 238.985 333.54 239.422C333.54 239.807 333.689 240.113 333.987 240.338C334.293 240.556 334.889 240.793 335.776 241.047C336.78 241.338 337.464 241.658 337.827 242.007C338.198 242.349 338.384 242.847 338.384 243.502C338.384 244.338 338.085 244.975 337.489 245.411C336.893 245.847 336.216 246.065 335.46 246.065ZM350.709 246.109C349.69 246.109 348.927 245.851 348.418 245.335C347.909 244.811 347.654 244.025 347.654 242.978V240.338C347.654 239.276 347.909 238.484 348.418 237.96C348.927 237.429 349.694 237.164 350.719 237.164C351.723 237.164 352.465 237.4 352.945 237.873C353.432 238.338 353.705 239.087 353.763 240.12L352.672 240.229C352.599 239.444 352.421 238.902 352.138 238.604C351.861 238.298 351.385 238.145 350.709 238.145C349.989 238.145 349.479 238.313 349.181 238.647C348.89 238.975 348.745 239.538 348.745 240.338V242.978C348.745 243.764 348.89 244.32 349.181 244.647C349.479 244.967 349.989 245.127 350.709 245.127C351.385 245.127 351.861 244.982 352.138 244.691C352.421 244.393 352.599 243.858 352.672 243.087L353.763 243.196C353.705 244.215 353.432 244.956 352.945 245.422C352.465 245.88 351.719 246.109 350.709 246.109ZM358.24 246.109C357.36 246.109 356.684 245.898 356.211 245.476C355.746 245.047 355.513 244.433 355.513 243.633V241.865C355.513 241.058 355.749 240.436 356.222 240C356.695 239.564 357.371 239.345 358.251 239.345C359.124 239.345 359.793 239.564 360.258 240C360.731 240.436 360.967 241.058 360.967 241.865V243.633C360.967 244.433 360.731 245.047 360.258 245.476C359.793 245.898 359.12 246.109 358.24 246.109ZM358.24 245.127C358.793 245.127 359.204 245.004 359.473 244.756C359.742 244.509 359.876 244.135 359.876 243.633V241.865C359.876 241.342 359.742 240.956 359.473 240.709C359.204 240.455 358.793 240.327 358.24 240.327C357.687 240.327 357.276 240.455 357.007 240.709C356.738 240.956 356.604 241.342 356.604 241.865V243.633C356.604 244.135 356.738 244.509 357.007 244.756C357.276 245.004 357.687 245.127 358.24 245.127ZM365.553 245.073C365.91 245.073 366.248 244.978 366.568 244.789C366.888 244.6 367.204 244.313 367.517 243.927V240.72C367.292 240.582 367.052 240.48 366.797 240.415C366.543 240.342 366.27 240.305 365.979 240.305C365.39 240.305 364.953 240.436 364.67 240.698C364.386 240.953 364.244 241.342 364.244 241.865V243.622C364.244 244.109 364.353 244.473 364.572 244.713C364.79 244.953 365.117 245.073 365.553 245.073ZM367.19 244.909C367.001 245.287 366.739 245.582 366.404 245.793C366.07 246.004 365.695 246.109 365.281 246.109C364.604 246.109 364.081 245.891 363.71 245.455C363.339 245.018 363.153 244.407 363.153 243.622V241.844C363.153 241.051 363.401 240.436 363.895 240C364.39 239.564 365.081 239.345 365.968 239.345C366.223 239.345 366.484 239.375 366.753 239.433C367.023 239.484 367.277 239.556 367.517 239.651V237.273H368.608V246H367.517V244.909H367.19ZM376.27 244.56C376.074 245.105 375.776 245.502 375.376 245.749C374.976 245.989 374.361 246.109 373.532 246.109C372.667 246.109 371.994 245.902 371.514 245.487C371.034 245.065 370.794 244.444 370.794 243.622V241.865C370.794 241.022 371.045 240.393 371.547 239.978C372.056 239.556 372.718 239.345 373.532 239.345C374.354 239.345 375.012 239.56 375.507 239.989C376.001 240.411 376.249 241.062 376.249 241.942V242.945H371.885V243.633C371.885 244.178 372.019 244.575 372.289 244.822C372.565 245.062 372.976 245.182 373.521 245.182C374.045 245.182 374.427 245.109 374.667 244.964C374.907 244.818 375.089 244.582 375.212 244.255L376.27 244.56ZM371.885 241.865V242.127H375.158V241.964C375.158 241.375 375.019 240.945 374.743 240.676C374.474 240.407 374.067 240.273 373.521 240.273C372.976 240.273 372.565 240.404 372.289 240.665C372.019 240.92 371.885 241.32 371.885 241.865ZM380.726 242.727L380.398 237.273H381.926L381.598 242.727H380.726ZM381.173 246.109C380.809 246.109 380.54 246.036 380.366 245.891C380.198 245.738 380.115 245.505 380.115 245.193V244.887C380.115 244.567 380.198 244.331 380.366 244.178C380.54 244.025 380.809 243.949 381.173 243.949C381.536 243.949 381.802 244.025 381.969 244.178C382.136 244.331 382.22 244.567 382.22 244.887V245.193C382.22 245.505 382.133 245.738 381.958 245.891C381.791 246.036 381.529 246.109 381.173 246.109ZM389.893 241.636L389.566 237.273H391.312L390.984 241.636H389.893ZM386.621 241.636L386.293 237.273H388.039L387.712 241.636H386.621Z" fill="var(--vscode-debugTokenExpression-string, #CE9178)" /> + <path d="M90.72 263.149C91.3309 263.149 91.7709 263.025 92.04 262.778C92.3164 262.531 92.4545 262.135 92.4545 261.589V259.822C92.4545 259.32 92.3491 258.956 92.1382 258.731C91.9345 258.498 91.6036 258.382 91.1455 258.382C90.7818 258.382 90.4436 258.473 90.1309 258.655C89.8182 258.836 89.5018 259.127 89.1818 259.527V262.735C89.4073 262.873 89.64 262.978 89.88 263.051C90.1273 263.116 90.4073 263.149 90.72 263.149ZM90.7309 264.109C90.4836 264.109 90.2218 264.08 89.9455 264.022C89.6764 263.971 89.4218 263.898 89.1818 263.804V266.182H88.0909V257.455H89.1818V258.545H89.5091C89.6909 258.175 89.9527 257.884 90.2945 257.673C90.6436 257.455 91.0182 257.345 91.4182 257.345C92.0727 257.345 92.5891 257.571 92.9673 258.022C93.3527 258.465 93.5455 259.069 93.5455 259.833V261.611C93.5455 262.382 93.2909 262.993 92.7818 263.444C92.28 263.887 91.5964 264.109 90.7309 264.109ZM96.277 264V257.455H97.3679V258.545H97.6952C97.8479 258.131 98.0806 257.829 98.3934 257.64C98.7061 257.444 99.0843 257.345 99.5279 257.345C100.015 257.345 100.422 257.509 100.75 257.836C101.077 258.156 101.241 258.611 101.241 259.2V260.182H100.204V259.593C100.204 259.178 100.11 258.873 99.9206 258.676C99.7388 258.48 99.4588 258.382 99.0806 258.382C98.7388 258.382 98.4261 258.465 98.1424 258.633C97.8661 258.8 97.6079 259.062 97.3679 259.418V264H96.277ZM103.372 264V263.018H105.554V258.436H103.372V257.455H106.645V263.018H108.827V264H103.372ZM106.099 256.255C105.823 256.255 105.598 256.211 105.423 256.124C105.256 256.036 105.172 255.873 105.172 255.633V255.556C105.172 255.309 105.256 255.142 105.423 255.055C105.598 254.96 105.823 254.913 106.099 254.913C106.376 254.913 106.598 254.96 106.765 255.055C106.939 255.142 107.027 255.309 107.027 255.556V255.633C107.027 255.873 106.939 256.036 106.765 256.124C106.59 256.211 106.369 256.255 106.099 256.255ZM111.013 264V257.455H112.104V258.545H112.431C112.591 258.175 112.842 257.884 113.184 257.673C113.526 257.455 113.915 257.345 114.351 257.345C115.006 257.345 115.522 257.545 115.9 257.945C116.278 258.345 116.467 258.891 116.467 259.582V264H115.376V259.8C115.376 259.342 115.264 258.993 115.038 258.753C114.82 258.505 114.504 258.382 114.089 258.382C113.762 258.382 113.427 258.48 113.086 258.676C112.751 258.873 112.424 259.156 112.104 259.527V264H111.013ZM122.461 264.098C121.741 264.098 121.181 263.92 120.781 263.564C120.381 263.207 120.181 262.662 120.181 261.927V258.436H118.326V257.455H120.181V255.273H121.272V257.455H124.108V258.436H121.272V261.829C121.272 262.251 121.377 262.564 121.588 262.767C121.799 262.971 122.112 263.073 122.526 263.073C122.802 263.073 123.061 263.055 123.301 263.018C123.541 262.975 123.77 262.92 123.988 262.855L124.206 263.847C123.959 263.92 123.693 263.978 123.41 264.022C123.133 264.073 122.817 264.098 122.461 264.098Z" fill="var(--vscode-debugConsole-warningForeground, #DCDCAA)" /> + <path d="M80.1055 306L78.8182 300.796L77.5309 306H76.44L75.2618 297.273H76.3418L77.1273 304.396L78.2727 299.455H79.4182L80.5636 304.396L81.3491 297.273H82.3745L81.1964 306H80.1055ZM89.2079 304.56C89.0115 305.105 88.7134 305.502 88.3134 305.749C87.9134 305.989 87.2988 306.109 86.4697 306.109C85.6043 306.109 84.9315 305.902 84.4515 305.487C83.9715 305.065 83.7315 304.444 83.7315 303.622V301.865C83.7315 301.022 83.9824 300.393 84.4843 299.978C84.9934 299.556 85.6552 299.345 86.4697 299.345C87.2915 299.345 87.9497 299.56 88.4443 299.989C88.9388 300.411 89.1861 301.062 89.1861 301.942V302.945H84.8224V303.633C84.8224 304.178 84.957 304.575 85.2261 304.822C85.5024 305.062 85.9134 305.182 86.4588 305.182C86.9824 305.182 87.3643 305.109 87.6043 304.964C87.8443 304.818 88.0261 304.582 88.1497 304.255L89.2079 304.56ZM84.8224 301.865V302.127H88.0952V301.964C88.0952 301.375 87.957 300.945 87.6806 300.676C87.4115 300.407 87.0043 300.273 86.4588 300.273C85.9134 300.273 85.5024 300.404 85.2261 300.665C84.957 300.92 84.8224 301.32 84.8224 301.865ZM91.3722 306V305.018H93.554V298.255H91.3722V297.273H94.6449V305.018H96.8267V306H91.3722ZM101.893 306.109C100.94 306.109 100.22 305.902 99.7328 305.487C99.2528 305.065 99.0128 304.447 99.0128 303.633V301.865C99.0128 301.036 99.2528 300.411 99.7328 299.989C100.22 299.56 100.936 299.345 101.882 299.345C102.726 299.345 103.373 299.538 103.824 299.924C104.282 300.302 104.536 300.858 104.587 301.593L103.518 301.658C103.489 301.207 103.34 300.873 103.071 300.655C102.802 300.436 102.402 300.327 101.871 300.327C101.275 300.327 100.831 300.455 100.54 300.709C100.249 300.956 100.104 301.342 100.104 301.865V303.633C100.104 304.135 100.249 304.509 100.54 304.756C100.838 305.004 101.289 305.127 101.893 305.127C102.416 305.127 102.813 305.036 103.082 304.855C103.351 304.665 103.496 304.385 103.518 304.015L104.587 304.08C104.536 304.749 104.286 305.255 103.835 305.596C103.384 305.938 102.736 306.109 101.893 306.109ZM109.381 306.109C108.501 306.109 107.824 305.898 107.352 305.476C106.886 305.047 106.653 304.433 106.653 303.633V301.865C106.653 301.058 106.89 300.436 107.363 300C107.835 299.564 108.512 299.345 109.392 299.345C110.264 299.345 110.933 299.564 111.399 300C111.872 300.436 112.108 301.058 112.108 301.865V303.633C112.108 304.433 111.872 305.047 111.399 305.476C110.933 305.898 110.261 306.109 109.381 306.109ZM109.381 305.127C109.933 305.127 110.344 305.004 110.613 304.756C110.883 304.509 111.017 304.135 111.017 303.633V301.865C111.017 301.342 110.883 300.956 110.613 300.709C110.344 300.455 109.933 300.327 109.381 300.327C108.828 300.327 108.417 300.455 108.148 300.709C107.879 300.956 107.744 301.342 107.744 301.865V303.633C107.744 304.135 107.879 304.509 108.148 304.756C108.417 305.004 108.828 305.127 109.381 305.127ZM114.861 301.309V306H113.858V299.455H114.861V300.327H115.079C115.218 300.022 115.399 299.782 115.625 299.607C115.85 299.433 116.087 299.345 116.334 299.345C116.618 299.345 116.854 299.433 117.043 299.607C117.239 299.775 117.374 300.015 117.447 300.327H117.687C117.825 300.022 118.01 299.782 118.243 299.607C118.483 299.433 118.738 299.345 119.007 299.345C119.37 299.345 119.658 299.495 119.869 299.793C120.079 300.084 120.185 300.48 120.185 300.982V306H119.181V301.156C119.181 300.829 119.138 300.582 119.05 300.415C118.963 300.247 118.836 300.164 118.669 300.164C118.53 300.164 118.356 300.265 118.145 300.469C117.941 300.673 117.734 300.953 117.523 301.309V306H116.519V301.156C116.519 300.829 116.472 300.582 116.378 300.415C116.29 300.247 116.163 300.164 115.996 300.164C115.85 300.164 115.679 300.262 115.483 300.458C115.294 300.655 115.087 300.938 114.861 301.309ZM127.411 304.56C127.215 305.105 126.916 305.502 126.516 305.749C126.116 305.989 125.502 306.109 124.673 306.109C123.807 306.109 123.135 305.902 122.655 305.487C122.175 305.065 121.935 304.444 121.935 303.622V301.865C121.935 301.022 122.186 300.393 122.687 299.978C123.196 299.556 123.858 299.345 124.673 299.345C125.495 299.345 126.153 299.56 126.647 299.989C127.142 300.411 127.389 301.062 127.389 301.942V302.945H123.026V303.633C123.026 304.178 123.16 304.575 123.429 304.822C123.706 305.062 124.116 305.182 124.662 305.182C125.186 305.182 125.567 305.109 125.807 304.964C126.047 304.818 126.229 304.582 126.353 304.255L127.411 304.56ZM123.026 301.865V302.127H126.298V301.964C126.298 301.375 126.16 300.945 125.884 300.676C125.615 300.407 125.207 300.273 124.662 300.273C124.116 300.273 123.706 300.404 123.429 300.665C123.16 300.92 123.026 301.32 123.026 301.865ZM141.023 306.098C140.303 306.098 139.743 305.92 139.343 305.564C138.943 305.207 138.743 304.662 138.743 303.927V300.436H136.889V299.455H138.743V297.273H139.834V299.455H142.67V300.436H139.834V303.829C139.834 304.251 139.94 304.564 140.15 304.767C140.361 304.971 140.674 305.073 141.089 305.073C141.365 305.073 141.623 305.055 141.863 305.018C142.103 304.975 142.332 304.92 142.55 304.855L142.769 305.847C142.521 305.92 142.256 305.978 141.972 306.022C141.696 306.073 141.38 306.098 141.023 306.098ZM147.584 306.109C146.704 306.109 146.027 305.898 145.555 305.476C145.089 305.047 144.857 304.433 144.857 303.633V301.865C144.857 301.058 145.093 300.436 145.566 300C146.038 299.564 146.715 299.345 147.595 299.345C148.467 299.345 149.137 299.564 149.602 300C150.075 300.436 150.311 301.058 150.311 301.865V303.633C150.311 304.433 150.075 305.047 149.602 305.476C149.137 305.898 148.464 306.109 147.584 306.109ZM147.584 305.127C148.137 305.127 148.547 305.004 148.817 304.756C149.086 304.509 149.22 304.135 149.22 303.633V301.865C149.22 301.342 149.086 300.956 148.817 300.709C148.547 300.455 148.137 300.327 147.584 300.327C147.031 300.327 146.62 300.455 146.351 300.709C146.082 300.956 145.947 301.342 145.947 301.865V303.633C145.947 304.135 146.082 304.509 146.351 304.756C146.62 305.004 147.031 305.127 147.584 305.127ZM164.032 306L161.12 298.647V306H160.138V297.273H161.632L164.611 304.789V297.273H165.592V306H164.032ZM170.506 306.109C169.626 306.109 168.949 305.898 168.477 305.476C168.011 305.047 167.778 304.433 167.778 303.633V301.865C167.778 301.058 168.015 300.436 168.488 300C168.96 299.564 169.637 299.345 170.517 299.345C171.389 299.345 172.058 299.564 172.524 300C172.997 300.436 173.233 301.058 173.233 301.865V303.633C173.233 304.433 172.997 305.047 172.524 305.476C172.058 305.898 171.386 306.109 170.506 306.109ZM170.506 305.127C171.058 305.127 171.469 305.004 171.738 304.756C172.008 304.509 172.142 304.135 172.142 303.633V301.865C172.142 301.342 172.008 300.956 171.738 300.709C171.469 300.455 171.058 300.327 170.506 300.327C169.953 300.327 169.542 300.455 169.273 300.709C169.004 300.956 168.869 301.342 168.869 301.865V303.633C168.869 304.135 169.004 304.509 169.273 304.756C169.542 305.004 169.953 305.127 170.506 305.127ZM179.226 306.098C178.506 306.098 177.946 305.92 177.546 305.564C177.146 305.207 176.946 304.662 176.946 303.927V300.436H175.092V299.455H176.946V297.273H178.037V299.455H180.874V300.436H178.037V303.829C178.037 304.251 178.143 304.564 178.354 304.767C178.564 304.971 178.877 305.073 179.292 305.073C179.568 305.073 179.826 305.055 180.066 305.018C180.306 304.975 180.535 304.92 180.754 304.855L180.972 305.847C180.724 305.92 180.459 305.978 180.175 306.022C179.899 306.073 179.583 306.098 179.226 306.098ZM188.536 304.56C188.34 305.105 188.041 305.502 187.641 305.749C187.241 305.989 186.627 306.109 185.798 306.109C184.932 306.109 184.26 305.902 183.78 305.487C183.3 305.065 183.06 304.444 183.06 303.622V301.865C183.06 301.022 183.311 300.393 183.812 299.978C184.321 299.556 184.983 299.345 185.798 299.345C186.62 299.345 187.278 299.56 187.772 299.989C188.267 300.411 188.514 301.062 188.514 301.942V302.945H184.151V303.633C184.151 304.178 184.285 304.575 184.554 304.822C184.831 305.062 185.241 305.182 185.787 305.182C186.311 305.182 186.692 305.109 186.932 304.964C187.172 304.818 187.354 304.582 187.478 304.255L188.536 304.56ZM184.151 301.865V302.127H187.423V301.964C187.423 301.375 187.285 300.945 187.009 300.676C186.74 300.407 186.332 300.273 185.787 300.273C185.241 300.273 184.831 300.404 184.554 300.665C184.285 300.92 184.151 301.32 184.151 301.865ZM193.34 306.109C192.904 306.109 192.453 306.044 191.988 305.913C191.522 305.782 191.093 305.593 190.7 305.345V297.273H191.791V300.545H192.118C192.308 300.167 192.569 299.873 192.904 299.662C193.238 299.451 193.613 299.345 194.028 299.345C194.704 299.345 195.228 299.564 195.598 300C195.969 300.436 196.155 301.047 196.155 301.833V303.611C196.155 304.404 195.908 305.018 195.413 305.455C194.918 305.891 194.228 306.109 193.34 306.109ZM193.329 305.149C193.918 305.149 194.355 305.022 194.638 304.767C194.922 304.505 195.064 304.113 195.064 303.589V301.844C195.064 301.349 194.955 300.982 194.737 300.742C194.518 300.502 194.191 300.382 193.755 300.382C193.398 300.382 193.06 300.476 192.74 300.665C192.42 300.855 192.104 301.142 191.791 301.527V304.735C192.024 304.873 192.264 304.978 192.511 305.051C192.766 305.116 193.038 305.149 193.329 305.149ZM201.068 306.109C200.188 306.109 199.512 305.898 199.039 305.476C198.574 305.047 198.341 304.433 198.341 303.633V301.865C198.341 301.058 198.577 300.436 199.05 300C199.523 299.564 200.199 299.345 201.079 299.345C201.952 299.345 202.621 299.564 203.086 300C203.559 300.436 203.795 301.058 203.795 301.865V303.633C203.795 304.433 203.559 305.047 203.086 305.476C202.621 305.898 201.948 306.109 201.068 306.109ZM201.068 305.127C201.621 305.127 202.032 305.004 202.301 304.756C202.57 304.509 202.705 304.135 202.705 303.633V301.865C202.705 301.342 202.57 300.956 202.301 300.709C202.032 300.455 201.621 300.327 201.068 300.327C200.515 300.327 200.105 300.455 199.835 300.709C199.566 300.956 199.432 301.342 199.432 301.865V303.633C199.432 304.135 199.566 304.509 199.835 304.756C200.105 305.004 200.515 305.127 201.068 305.127ZM208.709 306.109C207.829 306.109 207.152 305.898 206.68 305.476C206.214 305.047 205.982 304.433 205.982 303.633V301.865C205.982 301.058 206.218 300.436 206.691 300C207.163 299.564 207.84 299.345 208.72 299.345C209.592 299.345 210.262 299.564 210.727 300C211.2 300.436 211.436 301.058 211.436 301.865V303.633C211.436 304.433 211.2 305.047 210.727 305.476C210.262 305.898 209.589 306.109 208.709 306.109ZM208.709 305.127C209.262 305.127 209.672 305.004 209.942 304.756C210.211 304.509 210.345 304.135 210.345 303.633V301.865C210.345 301.342 210.211 300.956 209.942 300.709C209.672 300.455 209.262 300.327 208.709 300.327C208.156 300.327 207.745 300.455 207.476 300.709C207.207 300.956 207.072 301.342 207.072 301.865V303.633C207.072 304.135 207.207 304.509 207.476 304.756C207.745 305.004 208.156 305.127 208.709 305.127ZM215.259 303.022V306H214.168V297.273H215.259V302.105H215.815L217.909 299.455H219.24L216.677 302.498L219.208 306H217.964L215.815 303.022H215.259ZM224.077 306.109C223.525 306.109 222.99 306.029 222.474 305.869C221.957 305.702 221.481 305.447 221.045 305.105L221.536 304.244C221.892 304.535 222.288 304.756 222.725 304.909C223.168 305.055 223.619 305.127 224.077 305.127C224.456 305.127 224.816 305.069 225.157 304.953C225.506 304.836 225.681 304.589 225.681 304.211C225.681 303.898 225.539 303.676 225.256 303.545C224.972 303.415 224.452 303.302 223.696 303.207C222.896 303.113 222.288 302.927 221.874 302.651C221.466 302.367 221.263 301.924 221.263 301.32C221.263 300.665 221.51 300.175 222.005 299.847C222.499 299.513 223.074 299.345 223.728 299.345C224.245 299.345 224.739 299.433 225.212 299.607C225.692 299.782 226.136 300.029 226.543 300.349L226.052 301.211C225.717 300.942 225.354 300.727 224.961 300.567C224.568 300.407 224.157 300.327 223.728 300.327C223.379 300.327 223.059 300.393 222.768 300.524C222.485 300.655 222.343 300.895 222.343 301.244C222.343 301.542 222.474 301.76 222.736 301.898C223.005 302.029 223.517 302.142 224.274 302.236C225.103 302.331 225.725 302.535 226.139 302.847C226.554 303.153 226.761 303.582 226.761 304.135C226.761 304.818 226.485 305.32 225.932 305.64C225.379 305.953 224.761 306.109 224.077 306.109ZM236.544 306V305.018H238.726V300.436H236.544V299.455H239.817V305.018H241.999V306H236.544ZM239.271 298.255C238.995 298.255 238.769 298.211 238.595 298.124C238.428 298.036 238.344 297.873 238.344 297.633V297.556C238.344 297.309 238.428 297.142 238.595 297.055C238.769 296.96 238.995 296.913 239.271 296.913C239.548 296.913 239.769 296.96 239.937 297.055C240.111 297.142 240.199 297.309 240.199 297.556V297.633C240.199 297.873 240.111 298.036 239.937 298.124C239.762 298.211 239.54 298.255 239.271 298.255ZM244.185 306V299.455H245.276V300.545H245.603C245.763 300.175 246.014 299.884 246.356 299.673C246.697 299.455 247.086 299.345 247.523 299.345C248.177 299.345 248.694 299.545 249.072 299.945C249.45 300.345 249.639 300.891 249.639 301.582V306H248.548V301.8C248.548 301.342 248.436 300.993 248.21 300.753C247.992 300.505 247.676 300.382 247.261 300.382C246.934 300.382 246.599 300.48 246.257 300.676C245.923 300.873 245.596 301.156 245.276 301.527V306H244.185ZM262.771 306H261.626L258.811 297.273H259.99L262.215 304.615L264.44 297.273H265.586L262.771 306ZM269.976 306.065C269.321 306.065 268.71 305.96 268.143 305.749C267.576 305.538 267.067 305.153 266.616 304.593L267.401 303.851C267.707 304.265 268.07 304.575 268.492 304.778C268.921 304.982 269.427 305.084 270.008 305.084C270.547 305.084 270.979 304.96 271.307 304.713C271.641 304.458 271.808 304.116 271.808 303.687C271.808 303.273 271.667 302.956 271.383 302.738C271.099 302.513 270.539 302.276 269.703 302.029C268.641 301.724 267.917 301.393 267.532 301.036C267.147 300.68 266.954 300.175 266.954 299.52C266.954 298.764 267.205 298.193 267.707 297.807C268.216 297.422 268.83 297.229 269.55 297.229C270.161 297.229 270.739 297.338 271.285 297.556C271.837 297.767 272.325 298.145 272.747 298.691L271.994 299.444C271.703 299.029 271.339 298.72 270.903 298.516C270.467 298.313 270.023 298.211 269.572 298.211C269.114 298.211 268.747 298.305 268.47 298.495C268.194 298.676 268.056 298.985 268.056 299.422C268.056 299.807 268.205 300.113 268.503 300.338C268.808 300.556 269.405 300.793 270.292 301.047C271.296 301.338 271.979 301.658 272.343 302.007C272.714 302.349 272.899 302.847 272.899 303.502C272.899 304.338 272.601 304.975 272.005 305.411C271.408 305.847 270.732 306.065 269.976 306.065ZM285.224 306.109C284.206 306.109 283.442 305.851 282.933 305.335C282.424 304.811 282.17 304.025 282.17 302.978V300.338C282.17 299.276 282.424 298.484 282.933 297.96C283.442 297.429 284.21 297.164 285.235 297.164C286.239 297.164 286.981 297.4 287.461 297.873C287.948 298.338 288.221 299.087 288.279 300.12L287.188 300.229C287.115 299.444 286.937 298.902 286.653 298.604C286.377 298.298 285.901 298.145 285.224 298.145C284.504 298.145 283.995 298.313 283.697 298.647C283.406 298.975 283.261 299.538 283.261 300.338V302.978C283.261 303.764 283.406 304.32 283.697 304.647C283.995 304.967 284.504 305.127 285.224 305.127C285.901 305.127 286.377 304.982 286.653 304.691C286.937 304.393 287.115 303.858 287.188 303.087L288.279 303.196C288.221 304.215 287.948 304.956 287.461 305.422C286.981 305.88 286.235 306.109 285.224 306.109ZM292.756 306.109C291.876 306.109 291.199 305.898 290.727 305.476C290.261 305.047 290.028 304.433 290.028 303.633V301.865C290.028 301.058 290.265 300.436 290.738 300C291.21 299.564 291.887 299.345 292.767 299.345C293.639 299.345 294.308 299.564 294.774 300C295.247 300.436 295.483 301.058 295.483 301.865V303.633C295.483 304.433 295.247 305.047 294.774 305.476C294.308 305.898 293.636 306.109 292.756 306.109ZM292.756 305.127C293.308 305.127 293.719 305.004 293.988 304.756C294.258 304.509 294.392 304.135 294.392 303.633V301.865C294.392 301.342 294.258 300.956 293.988 300.709C293.719 300.455 293.308 300.327 292.756 300.327C292.203 300.327 291.792 300.455 291.523 300.709C291.254 300.956 291.119 301.342 291.119 301.865V303.633C291.119 304.135 291.254 304.509 291.523 304.756C291.792 305.004 292.203 305.127 292.756 305.127ZM300.069 305.073C300.425 305.073 300.764 304.978 301.084 304.789C301.404 304.6 301.72 304.313 302.033 303.927V300.72C301.807 300.582 301.567 300.48 301.313 300.415C301.058 300.342 300.785 300.305 300.494 300.305C299.905 300.305 299.469 300.436 299.185 300.698C298.902 300.953 298.76 301.342 298.76 301.865V303.622C298.76 304.109 298.869 304.473 299.087 304.713C299.305 304.953 299.633 305.073 300.069 305.073ZM301.705 304.909C301.516 305.287 301.254 305.582 300.92 305.793C300.585 306.004 300.211 306.109 299.796 306.109C299.12 306.109 298.596 305.891 298.225 305.455C297.854 305.018 297.669 304.407 297.669 303.622V301.844C297.669 301.051 297.916 300.436 298.411 300C298.905 299.564 299.596 299.345 300.484 299.345C300.738 299.345 301 299.375 301.269 299.433C301.538 299.484 301.793 299.556 302.033 299.651V297.273H303.124V306H302.033V304.909H301.705ZM310.786 304.56C310.59 305.105 310.291 305.502 309.891 305.749C309.491 305.989 308.877 306.109 308.048 306.109C307.182 306.109 306.51 305.902 306.03 305.487C305.55 305.065 305.31 304.444 305.31 303.622V301.865C305.31 301.022 305.561 300.393 306.062 299.978C306.571 299.556 307.233 299.345 308.048 299.345C308.87 299.345 309.528 299.56 310.022 299.989C310.517 300.411 310.764 301.062 310.764 301.942V302.945H306.401V303.633C306.401 304.178 306.535 304.575 306.804 304.822C307.081 305.062 307.491 305.182 308.037 305.182C308.561 305.182 308.942 305.109 309.182 304.964C309.422 304.818 309.604 304.582 309.728 304.255L310.786 304.56ZM306.401 301.865V302.127H309.673V301.964C309.673 301.375 309.535 300.945 309.259 300.676C308.99 300.407 308.582 300.273 308.037 300.273C307.491 300.273 307.081 300.404 306.804 300.665C306.535 300.92 306.401 301.32 306.401 301.865ZM315.241 302.727L314.914 297.273H316.441L316.114 302.727H315.241ZM315.688 306.109C315.325 306.109 315.056 306.036 314.881 305.891C314.714 305.738 314.63 305.505 314.63 305.193V304.887C314.63 304.567 314.714 304.331 314.881 304.178C315.056 304.025 315.325 303.949 315.688 303.949C316.052 303.949 316.318 304.025 316.485 304.178C316.652 304.331 316.736 304.567 316.736 304.887V305.193C316.736 305.505 316.648 305.738 316.474 305.891C316.307 306.036 316.045 306.109 315.688 306.109Z" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <path d="M51.7166 278.818V269.727H54.8984V270.545H52.6257V278.109H54.8984V278.818H51.7166ZM57.4474 277V276.182H59.4474V270.818H59.1838C58.9777 271.182 58.7111 271.439 58.3838 271.591C58.0626 271.742 57.6899 271.782 57.2656 271.709L57.1747 270.818C57.6414 270.873 58.0687 270.809 58.4565 270.627C58.8444 270.439 59.1838 270.139 59.4747 269.727H60.3565V276.182H62.1747V277H57.4474ZM63.9964 278.818V278.109H66.2692V270.545H63.9964V269.727H67.1783V278.818H63.9964Z" fill="var(--vscode-editorOverviewRuler-bracketMatchForeground, #A0A0A0)" /> + </g> + </g> + <rect width="520" height="39" transform="translate(12)" fill="var(--vscode-sideBar-background, #252526)" /> + <g clip-path="url(#clip0_30214831)"> + <rect width="115.654" height="39.2998" transform="translate(12)" fill="var(--vscode-editor-background, #1E1E1E)" /> + <rect x="25.4741" y="15.7197" width="88.7052" height="7.85995" rx="3.92998" fill="var(--vscode-list-activeSelectionIconForeground, #FFFFFF)" fill-opacity="0.12" /> + </g> + <g filter="url(#filter1_d_30214831)"> + <rect width="425" height="182" transform="translate(60 20)" fill="var(--vscode-sideBar-background, #252526)" /> + <rect x="67.5" y="27.5" width="410" height="25" fill="var(--vscode-checkbox-background, #3C3C3C)" /> + <path d="M73.168 43.3242V44.708L79.4648 41.9595V40.7598L73.168 38.0239V39.4204L77.8779 41.3691V41.4326L73.168 43.3242ZM81.1533 42.6768C81.1533 44.2129 82.2896 45.2222 83.9463 45.2222C85.7109 45.2222 86.7773 44.1748 86.7773 42.353V35.8403H85.3555V42.3467C85.3555 43.394 84.8413 43.9526 83.9336 43.9526C83.1211 43.9526 82.5688 43.4385 82.5498 42.6768H81.1533ZM94.7373 38.1064H93.3726V42.1499C93.3726 43.2607 92.7695 43.9336 91.665 43.9336C90.6621 43.9336 90.2114 43.3877 90.2114 42.2451V38.1064H88.8467V42.5815C88.8467 44.2002 89.6973 45.1333 91.2271 45.1333C92.2744 45.1333 92.979 44.689 93.3154 43.9082H93.4233V45H94.7373V38.1064ZM100.304 37.9858C99.3647 37.9858 98.5522 38.4619 98.1333 39.249H98.0317V38.1064H96.7178V47.2979H98.0825V43.959H98.1904C98.5522 44.689 99.333 45.1143 100.317 45.1143C102.062 45.1143 103.173 43.7305 103.173 41.5532C103.173 39.3633 102.062 37.9858 100.304 37.9858ZM99.917 43.9463C98.7744 43.9463 98.0571 43.0259 98.0571 41.5532C98.0571 40.0742 98.7744 39.1538 99.9233 39.1538C101.079 39.1538 101.771 40.0552 101.771 41.5532C101.771 43.0513 101.079 43.9463 99.917 43.9463ZM105.236 47.501C106.646 47.501 107.299 46.9741 107.89 45.3174L110.467 38.1064H109.02L107.293 43.6289H107.185L105.452 38.1064H103.967L106.468 45.019L106.366 45.3745C106.131 46.1045 105.763 46.3774 105.116 46.3774C104.989 46.3774 104.779 46.3711 104.671 46.3521V47.4692C104.798 47.4883 105.122 47.501 105.236 47.501ZM112.346 36.3672V38.1255H111.248V39.2173H112.346V43.1846C112.346 44.5112 112.949 45.0444 114.466 45.0444C114.732 45.0444 114.986 45.0127 115.208 44.9746V43.8892C115.018 43.9082 114.897 43.9209 114.688 43.9209C114.009 43.9209 113.71 43.5972 113.71 42.8545V39.2173H115.208V38.1255H113.71V36.3672H112.346ZM121.271 43.1528C121.017 43.7114 120.452 44.0161 119.626 44.0161C118.535 44.0161 117.83 43.229 117.786 41.9785V41.915H122.667V41.4453C122.667 39.2871 121.505 37.9731 119.582 37.9731C117.633 37.9731 116.396 39.376 116.396 41.5659C116.396 43.7686 117.608 45.1333 119.588 45.1333C121.169 45.1333 122.286 44.3716 122.584 43.1528H121.271ZM119.576 39.084C120.585 39.084 121.245 39.814 121.277 40.9312H117.786C117.862 39.8203 118.566 39.084 119.576 39.084ZM124.305 45H125.669V40.8804C125.669 39.9092 126.399 39.2427 127.434 39.2427C127.675 39.2427 128.082 39.2871 128.196 39.3188V38.0366C128.05 38.0049 127.79 37.9858 127.586 37.9858C126.685 37.9858 125.917 38.4746 125.72 39.1538H125.619V38.1064H124.305V45Z" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <rect x="67.5" y="27.5" width="410" height="25" stroke="var(--vscode-focusBorder, #007FD4)" /> + <rect opacity="0.4" x="72" y="66" width="38" height="6" rx="3" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <rect opacity="0.4" x="147" y="66" width="105" height="6" rx="3" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <rect x="114" y="66" width="29" height="6" rx="3" fill="var(--vscode-banner-iconForeground, #3794FF)" fill-opacity="0.6" /> + <rect width="425" height="24" transform="translate(60 82)" fill="var(--vscode-list-activeSelectionBackground, #062F4A)" /> + <path d="M72.4824 96.353C72.4824 98.1177 73.7139 99.2349 75.5928 99.2349C77.5352 99.2349 78.7031 98.1304 78.7031 96.1816V89.8403H76.7861V96.1689C76.7861 97.0894 76.3545 97.5781 75.5674 97.5781C74.8311 97.5781 74.3486 97.0957 74.3359 96.353H72.4824ZM87.082 92.0112H85.2349V96.0483C85.2349 97.0068 84.7397 97.5972 83.8193 97.5972C82.9688 97.5972 82.5308 97.0957 82.5308 96.1055V92.0112H80.6836V96.5625C80.6836 98.1875 81.623 99.146 83.1338 99.146C84.1938 99.146 84.8477 98.689 85.1777 97.8765H85.292V99H87.082V92.0112ZM92.979 91.897C92.0142 91.897 91.2207 92.373 90.8398 93.1602H90.7256V92.0112H88.9355V101.317H90.7827V97.9336H90.897C91.2397 98.6699 92.0078 99.1079 93.0107 99.1079C94.7627 99.1079 95.8354 97.7495 95.8354 95.5024C95.8354 93.249 94.75 91.897 92.979 91.897ZM92.3506 97.6162C91.373 97.6162 90.7637 96.8164 90.7637 95.5088C90.7637 94.2012 91.373 93.395 92.3569 93.395C93.3408 93.395 93.9375 94.1885 93.9375 95.5024C93.9375 96.8228 93.3472 97.6162 92.3506 97.6162ZM97.9175 101.539C99.6631 101.539 100.52 100.917 101.117 99.1333L103.529 92.0112H101.574L100.101 97.3433H99.9868L98.5142 92.0112H96.4766L98.9331 99.0381L98.8696 99.3047C98.7173 99.8696 98.3237 100.098 97.6318 100.098C97.5557 100.098 97.2764 100.092 97.2129 100.079V101.52C97.2891 101.533 97.8477 101.539 97.9175 101.539ZM105.344 90.3418V92.0747H104.252V93.4775H105.344V97.1147C105.344 98.4731 106.017 99.0254 107.718 99.0254C108.074 99.0254 108.417 98.9873 108.645 98.9429V97.5781C108.467 97.5972 108.34 97.6099 108.099 97.6099C107.471 97.6099 107.191 97.3179 107.191 96.6895V93.4775H108.645V92.0747H107.191V90.3418H105.344ZM114.663 97.0259C114.466 97.502 113.964 97.7686 113.234 97.7686C112.27 97.7686 111.66 97.1021 111.635 96.0356V95.9404H116.44V95.3882C116.44 93.1792 115.221 91.8589 113.158 91.8589C111.076 91.8589 109.794 93.2681 109.794 95.5405C109.794 97.8003 111.051 99.146 113.184 99.146C114.897 99.146 116.11 98.3271 116.383 97.0259H114.663ZM113.165 93.2363C114.028 93.2363 114.586 93.833 114.625 94.7915H111.641C111.705 93.8521 112.308 93.2363 113.165 93.2363ZM117.957 99H119.804V95.0898C119.804 94.106 120.496 93.5029 121.505 93.5029C121.804 93.5029 122.235 93.5537 122.381 93.6045V91.9795C122.223 91.9287 121.912 91.897 121.658 91.897C120.769 91.897 120.045 92.4238 119.861 93.1221H119.747V92.0112H117.957V99Z" fill="var(--vscode-list-activeSelectionForeground, #3794FF)" /> + <path d="M124.952 94.6519C125.638 94.6519 126.076 94.1948 126.076 93.5728C126.076 92.9507 125.638 92.5 124.952 92.5C124.273 92.5 123.829 92.9507 123.829 93.5728C123.829 94.1948 124.273 94.6519 124.952 94.6519ZM124.952 99.1587C125.638 99.1587 126.076 98.7017 126.076 98.0796C126.076 97.4575 125.638 97.0068 124.952 97.0068C124.273 97.0068 123.829 97.4575 123.829 98.0796C123.829 98.7017 124.273 99.1587 124.952 99.1587ZM135.673 99.2222C137.717 99.2222 139.222 98.0479 139.476 96.2642H138.06C137.806 97.3052 136.886 97.959 135.673 97.959C134.023 97.959 132.995 96.6006 132.995 94.4233C132.995 92.2461 134.023 90.8813 135.667 90.8813C136.873 90.8813 137.793 91.605 138.06 92.7412H139.476C139.247 90.9067 137.686 89.6182 135.667 89.6182C133.122 89.6182 131.541 91.459 131.541 94.4233C131.541 97.3813 133.128 99.2222 135.673 99.2222ZM141.189 99H142.554V94.8804C142.554 93.9092 143.284 93.2427 144.319 93.2427C144.56 93.2427 144.966 93.2871 145.081 93.3188V92.0366C144.935 92.0049 144.674 91.9858 144.471 91.9858C143.57 91.9858 142.802 92.4746 142.605 93.1538H142.503V92.1064H141.189V99ZM150.749 97.1528C150.495 97.7114 149.93 98.0161 149.105 98.0161C148.013 98.0161 147.309 97.229 147.264 95.9785V95.915H152.146V95.4453C152.146 93.2871 150.984 91.9731 149.061 91.9731C147.112 91.9731 145.874 93.376 145.874 95.5659C145.874 97.7686 147.086 99.1333 149.067 99.1333C150.647 99.1333 151.765 98.3716 152.063 97.1528H150.749ZM149.054 93.084C150.063 93.084 150.724 93.814 150.755 94.9312H147.264C147.34 93.8203 148.045 93.084 149.054 93.084ZM155.694 99.1143C156.602 99.1143 157.357 98.7207 157.77 98.0225H157.877V99H159.191V94.2837C159.191 92.8364 158.214 91.9731 156.481 91.9731C154.913 91.9731 153.796 92.7285 153.656 93.8901H154.977C155.129 93.3887 155.656 93.103 156.417 93.103C157.351 93.103 157.833 93.5283 157.833 94.2837V94.8867L155.96 95.001C154.316 95.1025 153.39 95.8198 153.39 97.0576C153.39 98.3145 154.361 99.1143 155.694 99.1143ZM156.043 98.0161C155.3 98.0161 154.761 97.6416 154.761 97.0005C154.761 96.3721 155.192 96.0356 156.145 95.9722L157.833 95.8579V96.4546C157.833 97.3433 157.071 98.0161 156.043 98.0161ZM161.616 90.3672V92.1255H160.518V93.2173H161.616V97.1846C161.616 98.5112 162.219 99.0444 163.736 99.0444C164.003 99.0444 164.257 99.0127 164.479 98.9746V97.8892C164.289 97.9082 164.168 97.9209 163.958 97.9209C163.279 97.9209 162.981 97.5972 162.981 96.8545V93.2173H164.479V92.1255H162.981V90.3672H161.616ZM170.541 97.1528C170.287 97.7114 169.722 98.0161 168.897 98.0161C167.805 98.0161 167.101 97.229 167.056 95.9785V95.915H171.938V95.4453C171.938 93.2871 170.776 91.9731 168.853 91.9731C166.904 91.9731 165.666 93.376 165.666 95.5659C165.666 97.7686 166.878 99.1333 168.859 99.1333C170.439 99.1333 171.557 98.3716 171.855 97.1528H170.541ZM168.846 93.084C169.855 93.084 170.516 93.814 170.547 94.9312H167.056C167.132 93.8203 167.837 93.084 168.846 93.084ZM178.672 99V92.3477H178.774L183.478 99H184.76V89.8403H183.376V96.5054H183.274L178.571 89.8403H177.289V99H178.672ZM191.438 97.1528C191.184 97.7114 190.619 98.0161 189.793 98.0161C188.702 98.0161 187.997 97.229 187.953 95.9785V95.915H192.834V95.4453C192.834 93.2871 191.672 91.9731 189.749 91.9731C187.8 91.9731 186.562 93.376 186.562 95.5659C186.562 97.7686 187.775 99.1333 189.755 99.1333C191.336 99.1333 192.453 98.3716 192.751 97.1528H191.438ZM189.743 93.084C190.752 93.084 191.412 93.814 191.444 94.9312H187.953C188.029 93.8203 188.733 93.084 189.743 93.084ZM203.422 92.1064H202.057L200.826 97.4258H200.718L199.296 92.1064H197.988L196.566 97.4258H196.465L195.227 92.1064H193.843L195.748 99H197.15L198.572 93.8584H198.68L200.108 99H201.524L203.422 92.1064ZM207.916 96.6768C207.916 98.2129 209.052 99.2222 210.709 99.2222C212.474 99.2222 213.54 98.1748 213.54 96.353V89.8403H212.118V96.3467C212.118 97.394 211.604 97.9526 210.696 97.9526C209.884 97.9526 209.332 97.4385 209.312 96.6768H207.916ZM221.5 92.1064H220.135V96.1499C220.135 97.2607 219.532 97.9336 218.428 97.9336C217.425 97.9336 216.974 97.3877 216.974 96.2451V92.1064H215.609V96.5815C215.609 98.2002 216.46 99.1333 217.99 99.1333C219.037 99.1333 219.742 98.689 220.078 97.9082H220.186V99H221.5V92.1064ZM227.067 91.9858C226.127 91.9858 225.315 92.4619 224.896 93.249H224.794V92.1064H223.48V101.298H224.845V97.959H224.953C225.315 98.689 226.096 99.1143 227.08 99.1143C228.825 99.1143 229.936 97.7305 229.936 95.5532C229.936 93.3633 228.825 91.9858 227.067 91.9858ZM226.68 97.9463C225.537 97.9463 224.82 97.0259 224.82 95.5532C224.82 94.0742 225.537 93.1538 226.686 93.1538C227.841 93.1538 228.533 94.0552 228.533 95.5532C228.533 97.0513 227.841 97.9463 226.68 97.9463ZM231.999 101.501C233.408 101.501 234.062 100.974 234.652 99.3174L237.229 92.1064H235.782L234.056 97.6289H233.948L232.215 92.1064H230.729L233.23 99.019L233.129 99.3745C232.894 100.104 232.526 100.377 231.878 100.377C231.751 100.377 231.542 100.371 231.434 100.352V101.469C231.561 101.488 231.885 101.501 231.999 101.501ZM239.108 90.3672V92.1255H238.01V93.2173H239.108V97.1846C239.108 98.5112 239.711 99.0444 241.229 99.0444C241.495 99.0444 241.749 99.0127 241.971 98.9746V97.8892C241.781 97.9082 241.66 97.9209 241.451 97.9209C240.771 97.9209 240.473 97.5972 240.473 96.8545V93.2173H241.971V92.1255H240.473V90.3672H239.108ZM248.033 97.1528C247.779 97.7114 247.214 98.0161 246.389 98.0161C245.297 98.0161 244.593 97.229 244.548 95.9785V95.915H249.43V95.4453C249.43 93.2871 248.268 91.9731 246.345 91.9731C244.396 91.9731 243.158 93.376 243.158 95.5659C243.158 97.7686 244.371 99.1333 246.351 99.1333C247.932 99.1333 249.049 98.3716 249.347 97.1528H248.033ZM246.338 93.084C247.348 93.084 248.008 93.814 248.04 94.9312H244.548C244.625 93.8203 245.329 93.084 246.338 93.084ZM251.067 99H252.432V94.8804C252.432 93.9092 253.162 93.2427 254.197 93.2427C254.438 93.2427 254.844 93.2871 254.958 93.3188V92.0366C254.812 92.0049 254.552 91.9858 254.349 91.9858C253.448 91.9858 252.68 92.4746 252.483 93.1538H252.381V92.1064H251.067V99ZM261.37 99V92.3477H261.471L266.175 99H267.457V89.8403H266.073V96.5054H265.972L261.268 89.8403H259.986V99H261.37ZM272.522 99.1333C274.541 99.1333 275.779 97.7812 275.779 95.5532C275.779 93.3252 274.535 91.9731 272.522 91.9731C270.504 91.9731 269.26 93.3315 269.26 95.5532C269.26 97.7812 270.498 99.1333 272.522 99.1333ZM272.522 97.9717C271.335 97.9717 270.669 97.0894 270.669 95.5532C270.669 94.0171 271.335 93.1284 272.522 93.1284C273.703 93.1284 274.376 94.0171 274.376 95.5532C274.376 97.083 273.703 97.9717 272.522 97.9717ZM277.905 90.3672V92.1255H276.807V93.2173H277.905V97.1846C277.905 98.5112 278.508 99.0444 280.025 99.0444C280.292 99.0444 280.546 99.0127 280.768 98.9746V97.8892C280.578 97.9082 280.457 97.9209 280.248 97.9209C279.568 97.9209 279.27 97.5972 279.27 96.8545V93.2173H280.768V92.1255H279.27V90.3672H277.905ZM286.83 97.1528C286.576 97.7114 286.011 98.0161 285.186 98.0161C284.094 98.0161 283.39 97.229 283.345 95.9785V95.915H288.227V95.4453C288.227 93.2871 287.065 91.9731 285.142 91.9731C283.193 91.9731 281.955 93.376 281.955 95.5659C281.955 97.7686 283.167 99.1333 285.148 99.1333C286.729 99.1333 287.846 98.3716 288.144 97.1528H286.83ZM285.135 93.084C286.145 93.084 286.805 93.814 286.836 94.9312H283.345C283.421 93.8203 284.126 93.084 285.135 93.084ZM293.527 99.1143C295.272 99.1143 296.383 97.7241 296.383 95.5532C296.383 93.3633 295.279 91.9858 293.527 91.9858C292.581 91.9858 291.769 92.4492 291.4 93.1982H291.292V89.4023H289.928V99H291.242V97.9082H291.343C291.756 98.6699 292.562 99.1143 293.527 99.1143ZM293.133 93.1538C294.289 93.1538 294.98 94.0615 294.98 95.5532C294.98 97.0449 294.289 97.9463 293.133 97.9463C291.984 97.9463 291.273 97.0322 291.267 95.5532C291.273 94.0742 291.991 93.1538 293.133 93.1538ZM300.947 99.1333C302.966 99.1333 304.204 97.7812 304.204 95.5532C304.204 93.3252 302.959 91.9731 300.947 91.9731C298.929 91.9731 297.685 93.3315 297.685 95.5532C297.685 97.7812 298.922 99.1333 300.947 99.1333ZM300.947 97.9717C299.76 97.9717 299.094 97.0894 299.094 95.5532C299.094 94.0171 299.76 93.1284 300.947 93.1284C302.128 93.1284 302.801 94.0171 302.801 95.5532C302.801 97.083 302.128 97.9717 300.947 97.9717ZM308.768 99.1333C310.786 99.1333 312.024 97.7812 312.024 95.5532C312.024 93.3252 310.78 91.9731 308.768 91.9731C306.749 91.9731 305.505 93.3315 305.505 95.5532C305.505 97.7812 306.743 99.1333 308.768 99.1333ZM308.768 97.9717C307.581 97.9717 306.914 97.0894 306.914 95.5532C306.914 94.0171 307.581 93.1284 308.768 93.1284C309.948 93.1284 310.621 94.0171 310.621 95.5532C310.621 97.083 309.948 97.9717 308.768 97.9717ZM315.204 95.0137H315.096V89.4023H313.731V99H315.096V96.4927L315.699 95.915L318.073 99H319.749L316.715 95.0581L319.559 92.1064H317.946L315.204 95.0137Z" fill="var(--vscode-list-activeSelectionForeground, #E3E3E3)" /> + <rect opacity="0.4" x="72" y="116" width="62.5" height="6" rx="3" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <rect x="138.5" y="116" width="29" height="6" rx="3" fill="var(--vscode-banner-iconForeground, #3794FF)" fill-opacity="0.6" /> + <rect opacity="0.4" x="171.5" y="116" width="62.5" height="6" rx="3" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <rect opacity="0.4" x="72" y="138" width="41" height="6" rx="3" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <rect opacity="0.4" x="117" y="138" width="69" height="6" rx="3" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <rect x="190" y="138" width="26" height="6" rx="3" fill="var(--vscode-banner-iconForeground, #3794FF)" fill-opacity="0.6" /> + <rect x="72" y="160" width="26" height="6" rx="3" fill="var(--vscode-banner-iconForeground, #3794FF)" fill-opacity="0.6" /> + <rect opacity="0.4" x="102" y="160" width="164" height="6" rx="3" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <rect opacity="0.4" x="72" y="182" width="46" height="6" rx="3" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <rect x="122" y="182" width="29" height="6" rx="3" fill="var(--vscode-banner-iconForeground, #3794FF)" fill-opacity="0.6" /> + <rect opacity="0.4" x="155" y="182" width="46" height="6" rx="3" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + </g> + <defs> + <filter id="filter0_d_30214831" x="0" y="179" width="543" height="158" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> + <feFlood flood-opacity="0" result="BackgroundImageFix" /> + <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> + <feOffset dy="2" /> + <feGaussianBlur stdDeviation="6" /> + <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0" /> + <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_30214831" /> + <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_30214831" result="shape" /> + </filter> + <filter id="filter1_d_30214831" x="48" y="10" width="449" height="206" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> + <feFlood flood-opacity="0" result="BackgroundImageFix" /> + <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> + <feOffset dy="2" /> + <feGaussianBlur stdDeviation="6" /> + <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0" /> + <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_30214831" /> + <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_30214831" result="shape" /> + </filter> + <clipPath id="clip0_30214831"> + <rect width="115.654" height="39.2998" fill="var(--vscode-list-activeSelectionIconForeground, #FFFFFF)" transform="translate(12)" /> + </clipPath> + </defs> +</svg> \ 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 @@ +<svg width="566" height="187" viewBox="0 0 566 187" fill="none" xmlns="http://www.w3.org/2000/svg"> + <rect width="502" height="39" transform="translate(32)" fill="var(--vscode-editorGroupHeader-tabsBackground, #292929)" /> + <g clip-path="url(#clip0_30614476)"> + <rect width="136.145" height="39.2998" transform="translate(32)" fill="var(--vscode-tab-activeBackground, #252526)" /> + <path d="M55.6541 14.0294C56.0008 14.0294 56.2941 14.1561 56.5341 14.4094C56.7875 14.6494 56.9141 14.9427 56.9141 15.2894V24.0094C56.9141 24.3561 56.7875 24.6561 56.5341 24.9094C56.2941 25.1494 56.0008 25.2694 55.6541 25.2694H49.7341C49.3875 25.2694 49.0875 25.1494 48.8341 24.9094C48.5941 24.6561 48.4741 24.3561 48.4741 24.0094V15.2894C48.4741 14.9427 48.5941 14.6494 48.8341 14.4094C49.0875 14.1561 49.3875 14.0294 49.7341 14.0294H55.6541ZM57.4741 21.3294H57.8941C58.0008 21.3294 58.0941 21.3694 58.1741 21.4494C58.2541 21.5161 58.3008 21.6027 58.3141 21.7094V22.6094C58.3141 22.7161 58.2808 22.8094 58.2141 22.8894C58.1475 22.9561 58.0608 23.0027 57.9541 23.0294H57.4741V21.3294H57.8941H57.4741ZM57.4741 19.0894H57.8941C58.0008 19.0894 58.0941 19.1227 58.1741 19.1894C58.2541 19.2561 58.3008 19.3427 58.3141 19.4494V20.3494C58.3141 20.4561 58.2808 20.5494 58.2141 20.6294C58.1475 20.7094 58.0608 20.7561 57.9541 20.7694H57.4741V19.0894H57.8941H57.4741ZM57.4741 16.8294H57.8941C58.0008 16.8294 58.0941 16.8694 58.1741 16.9494C58.2541 17.0161 58.3008 17.1027 58.3141 17.2094V18.1094C58.3141 18.2161 58.2808 18.3094 58.2141 18.3894C58.1475 18.4561 58.0608 18.5027 57.9541 18.5294H57.4741V16.8294H57.8941H57.4741ZM54.5141 15.9894H50.8741C50.7675 15.9894 50.6741 16.0294 50.5941 16.1094C50.5141 16.1761 50.4675 16.2561 50.4541 16.3494L50.4341 17.2694C50.4341 17.3627 50.4675 17.4494 50.5341 17.5294C50.6141 17.6094 50.7075 17.6561 50.8141 17.6694L54.5141 17.6894C54.6208 17.6894 54.7141 17.6561 54.7941 17.5894C54.8741 17.5094 54.9208 17.4161 54.9341 17.3094L54.9541 16.4094C54.9541 16.2894 54.9075 16.1894 54.8141 16.1094C54.7341 16.0294 54.6341 15.9894 54.5141 15.9894Z" fill="#519ABA" /> + <rect x="65.9656" y="15.7197" width="88.7052" height="7.85995" rx="3.92998" fill="var(--vscode-foreground, #FFFFFF)" fill-opacity="0.12" /> + </g> + <g filter="url(#filter0_d_30614476)"> + <rect width="542" height="134" transform="translate(12 39)" fill="var(--vscode-editor-background, #292929)" /> + <rect x="25" y="71" width="4" height="90" rx="2" fill="var(--vscode-focusBorder, #007FD4)" /> + <path d="M40.7867 75.9733L39.9867 76.4V88.4L40.7867 88.8267L49.8 82.8533V82L40.7867 75.9733ZM41 87.4933V77.36L48.6267 82.4267L41 87.4933Z" fill="var(--vscode-foreground, #CCCCCC)" /> + <path d="M63 84.08L67.32 79.7067L67.96 80.3467L63.2667 84.9867H62.68L57.9867 80.3467L58.6267 79.7067L63 84.08Z" fill="var(--vscode-foreground, #CCCCCC)" /> + <rect x="75.5" y="71.5" width="460" height="59" fill="var(--vscode-sideBar-background, #303031)" stroke="var(--vscode-notebook-cellBorderColor, #37373d)" /> + <rect x="374.5" y="55.5" width="152" height="28" fill="var(--vscode-sideBar-background, #303031)" stroke="var(--vscode-notebook-cellBorderColor, #37373d)" /> + <path d="M408.76 63.0133L408.013 63.44V75.44L408.76 75.8133L417.773 69.84V68.9867L408.76 63.0133ZM409.027 74.48V64.3467L416.6 69.4133L409.027 74.48ZM419.16 70H419.853L422.36 72.5067L421.667 73.2L420.013 71.5467V76.9867H419V71.5467L417.347 73.2L416.653 72.5067L419.16 70Z" fill="var(--vscode-foreground, #CCCCCC)" /> + <path d="M432.813 63.0133L432.013 63.44V75.44L432.813 75.8133L441.827 69.84V68.9867L432.813 63.0133ZM433.027 74.48V64.3467L440.6 69.4133L433.027 74.48ZM443.853 76.9867H443.16L440.653 74.48L441.347 73.7867L443 75.44V70H444.013V75.44L445.667 73.7867L446.36 74.5333L443.853 76.9867Z" fill="var(--vscode-foreground, #CCCCCC)" /> + <path d="M483 70C483 70.2844 482.893 70.5333 482.68 70.7467C482.502 70.9244 482.271 71.0133 481.987 71.0133C481.702 71.0133 481.471 70.9244 481.293 70.7467C481.116 70.5333 481.027 70.2844 481.027 70C481.027 69.7156 481.116 69.4844 481.293 69.3067C481.471 69.0933 481.702 68.9867 481.987 68.9867C482.271 68.9867 482.502 69.0933 482.68 69.3067C482.893 69.4844 483 69.7156 483 70ZM488.013 70C488.013 70.2844 487.907 70.5333 487.693 70.7467C487.516 70.9244 487.284 71.0133 487 71.0133C486.716 71.0133 486.467 70.9244 486.253 70.7467C486.076 70.5333 485.987 70.2844 485.987 70C485.987 69.7156 486.076 69.4844 486.253 69.3067C486.467 69.0933 486.716 68.9867 487 68.9867C487.284 68.9867 487.516 69.0933 487.693 69.3067C487.907 69.4844 488.013 69.7156 488.013 70ZM493.027 70C493.027 70.2844 492.92 70.5333 492.707 70.7467C492.529 70.9244 492.298 71.0133 492.013 71.0133C491.729 71.0133 491.48 70.9244 491.267 70.7467C491.089 70.5333 491 70.2844 491 70C491 69.7156 491.089 69.4844 491.267 69.3067C491.48 69.0933 491.729 68.9867 492.013 68.9867C492.298 68.9867 492.529 69.0933 492.707 69.3067C492.92 69.4844 493.027 69.7156 493.027 70Z" fill="var(--vscode-foreground, #CCCCCC)" /> + <path d="M513.027 64.9867H516.013V66H515V75.0133L513.987 75.9733H507L505.987 75.0133V66H505.027V64.9867H508.013V63.9733C508.013 63.7244 508.102 63.5111 508.28 63.3333C508.493 63.12 508.742 63.0133 509.027 63.0133H512.013C512.298 63.0133 512.529 63.12 512.707 63.3333C512.92 63.5111 513.027 63.7244 513.027 63.9733V64.9867ZM512.013 63.9733H509.027V64.9867H512.013V63.9733ZM507 75.0133H513.987V66H507V75.0133ZM509.027 67.0133H508.013V74H509.027V67.0133ZM509.987 67.0133H511V74H509.987V67.0133ZM512.013 67.0133H513.027V74H512.013V67.0133Z" fill="var(--vscode-foreground, #CCCCCC)" /> + <path d="M389.027 71.9733V71.0133H397.987V71.9733H389.027ZM393.027 67.9733H397.987V68.9867H393.027V67.9733ZM397.987 64.9867V66H389.027V64.9867H397.987ZM389.027 74V75.0133H397.987V74H389.027ZM384.013 64.7733L384.76 64.4L390.147 67.9733V68.7733L384.76 72.4L384.013 71.9733V64.7733ZM385.027 65.7333V71.0133L388.973 68.4L385.027 65.7333Z" fill="var(--vscode-foreground, #CCCCCC)" /> + <path d="M469.027 63.0133H457.987L457.027 63.9733V75.0133L457.987 75.9733H469.027L469.987 75.0133V63.9733L469.027 63.0133ZM469.027 75.0133H457.987V70H469.027V75.0133ZM469.027 68.9867H457.987V63.9733H469.027V68.9867Z" fill="var(--vscode-foreground, #CCCCCC)" /> + <path d="M88.6582 91.3091V96H87.6545V89.4545H88.6582V90.3273H88.8764C89.0145 90.0218 89.1964 89.7818 89.4218 89.6073C89.6473 89.4327 89.8836 89.3455 90.1309 89.3455C90.4145 89.3455 90.6509 89.4327 90.84 89.6073C91.0364 89.7745 91.1709 90.0145 91.2436 90.3273H91.4836C91.6218 90.0218 91.8073 89.7818 92.04 89.6073C92.28 89.4327 92.5345 89.3455 92.8036 89.3455C93.1673 89.3455 93.4545 89.4945 93.6655 89.7927C93.8764 90.0836 93.9818 90.48 93.9818 90.9818V96H92.9782V91.1564C92.9782 90.8291 92.9345 90.5818 92.8473 90.4145C92.76 90.2473 92.6327 90.1636 92.4655 90.1636C92.3273 90.1636 92.1527 90.2655 91.9418 90.4691C91.7382 90.6727 91.5309 90.9527 91.32 91.3091V96H90.3164V91.1564C90.3164 90.8291 90.2691 90.5818 90.1745 90.4145C90.0873 90.2473 89.96 90.1636 89.7927 90.1636C89.6473 90.1636 89.4764 90.2618 89.28 90.4582C89.0909 90.6545 88.8836 90.9382 88.6582 91.3091ZM98.5461 96.1091C97.9934 96.1091 97.4588 96.0291 96.9424 95.8691C96.4261 95.7018 95.9497 95.4473 95.5134 95.1055L96.0043 94.2436C96.3606 94.5345 96.757 94.7564 97.1934 94.9091C97.637 95.0545 98.0879 95.1273 98.5461 95.1273C98.9243 95.1273 99.2843 95.0691 99.6261 94.9527C99.9752 94.8364 100.15 94.5891 100.15 94.2109C100.15 93.8982 100.008 93.6764 99.7243 93.5455C99.4406 93.4145 98.9206 93.3018 98.1643 93.2073C97.3643 93.1127 96.757 92.9273 96.3424 92.6509C95.9352 92.3673 95.7315 91.9236 95.7315 91.32C95.7315 90.6655 95.9788 90.1745 96.4734 89.8473C96.9679 89.5127 97.5424 89.3455 98.197 89.3455C98.7134 89.3455 99.2079 89.4327 99.6806 89.6073C100.161 89.7818 100.604 90.0291 101.012 90.3491L100.521 91.2109C100.186 90.9418 99.8224 90.7273 99.4297 90.5673C99.037 90.4073 98.6261 90.3273 98.197 90.3273C97.8479 90.3273 97.5279 90.3927 97.237 90.5236C96.9534 90.6545 96.8115 90.8945 96.8115 91.2436C96.8115 91.5418 96.9424 91.76 97.2043 91.8982C97.4734 92.0291 97.9861 92.1418 98.7424 92.2364C99.5715 92.3309 100.193 92.5345 100.608 92.8473C101.022 93.1527 101.23 93.5818 101.23 94.1345C101.23 94.8182 100.953 95.32 100.401 95.64C99.8479 95.9527 99.2297 96.1091 98.5461 96.1091ZM109.372 96.5236C109.372 97.1127 109.154 97.5527 108.718 97.8436C108.289 98.1418 107.761 98.2909 107.136 98.2909H105.085C104.43 98.2909 103.889 98.16 103.459 97.8982C103.038 97.6364 102.827 97.2327 102.827 96.6873C102.827 96.3382 102.918 96.0618 103.099 95.8582C103.289 95.6473 103.561 95.5273 103.918 95.4982V95.2691C103.663 95.2255 103.452 95.12 103.285 94.9527C103.125 94.7855 103.045 94.5273 103.045 94.1782C103.045 93.8655 103.143 93.5855 103.339 93.3382C103.536 93.0836 103.867 92.9382 104.332 92.9018V92.6945C103.976 92.5782 103.736 92.4036 103.612 92.1709C103.489 91.9309 103.427 91.7018 103.427 91.4836V91.1891C103.427 90.6873 103.641 90.2727 104.07 89.9455C104.499 89.6182 105.103 89.4545 105.881 89.4545H109.099V90.4364L107.409 90.1855V90.3818C107.699 90.4618 107.918 90.6145 108.063 90.84C108.216 91.0655 108.292 91.2909 108.292 91.5164V91.8C108.292 92.3018 108.099 92.7091 107.714 93.0218C107.329 93.3273 106.703 93.48 105.838 93.48H104.518C104.321 93.48 104.172 93.5309 104.07 93.6327C103.969 93.7273 103.918 93.8909 103.918 94.1236C103.918 94.3491 103.987 94.52 104.125 94.6364C104.27 94.7455 104.485 94.8 104.769 94.8H107.245C107.856 94.8 108.361 94.9345 108.761 95.2036C109.169 95.4727 109.372 95.9127 109.372 96.5236ZM105.881 92.6509C106.325 92.6509 106.663 92.5709 106.896 92.4109C107.129 92.2509 107.245 92 107.245 91.6582V91.3091C107.245 90.9673 107.129 90.72 106.896 90.5673C106.663 90.4073 106.325 90.3273 105.881 90.3273C105.438 90.3273 105.099 90.4073 104.867 90.5673C104.634 90.72 104.518 90.9673 104.518 91.3091V91.6582C104.518 92 104.634 92.2509 104.867 92.4109C105.099 92.5709 105.438 92.6509 105.881 92.6509ZM105.063 97.4182H107.278C107.605 97.4182 107.87 97.3564 108.074 97.2327C108.278 97.1091 108.379 96.8945 108.379 96.5891C108.379 96.2982 108.285 96.0909 108.096 95.9673C107.914 95.8436 107.678 95.7818 107.387 95.7818H103.918V96.5236C103.918 96.8218 104.016 97.0436 104.212 97.1891C104.409 97.3418 104.692 97.4182 105.063 97.4182ZM134.502 109.309V114H133.498V107.455H134.502V108.327H134.72C134.858 108.022 135.04 107.782 135.266 107.607C135.491 107.433 135.727 107.345 135.975 107.345C136.258 107.345 136.495 107.433 136.684 107.607C136.88 107.775 137.015 108.015 137.087 108.327H137.327C137.466 108.022 137.651 107.782 137.884 107.607C138.124 107.433 138.378 107.345 138.647 107.345C139.011 107.345 139.298 107.495 139.509 107.793C139.72 108.084 139.826 108.48 139.826 108.982V114H138.822V109.156C138.822 108.829 138.778 108.582 138.691 108.415C138.604 108.247 138.476 108.164 138.309 108.164C138.171 108.164 137.996 108.265 137.786 108.469C137.582 108.673 137.375 108.953 137.164 109.309V114H136.16V109.156C136.16 108.829 136.113 108.582 136.018 108.415C135.931 108.247 135.804 108.164 135.636 108.164C135.491 108.164 135.32 108.262 135.124 108.458C134.935 108.655 134.727 108.938 134.502 109.309ZM144.39 114.109C143.837 114.109 143.303 114.029 142.786 113.869C142.27 113.702 141.793 113.447 141.357 113.105L141.848 112.244C142.204 112.535 142.601 112.756 143.037 112.909C143.481 113.055 143.932 113.127 144.39 113.127C144.768 113.127 145.128 113.069 145.47 112.953C145.819 112.836 145.993 112.589 145.993 112.211C145.993 111.898 145.852 111.676 145.568 111.545C145.284 111.415 144.764 111.302 144.008 111.207C143.208 111.113 142.601 110.927 142.186 110.651C141.779 110.367 141.575 109.924 141.575 109.32C141.575 108.665 141.823 108.175 142.317 107.847C142.812 107.513 143.386 107.345 144.041 107.345C144.557 107.345 145.052 107.433 145.524 107.607C146.004 107.782 146.448 108.029 146.855 108.349L146.364 109.211C146.03 108.942 145.666 108.727 145.273 108.567C144.881 108.407 144.47 108.327 144.041 108.327C143.692 108.327 143.372 108.393 143.081 108.524C142.797 108.655 142.655 108.895 142.655 109.244C142.655 109.542 142.786 109.76 143.048 109.898C143.317 110.029 143.83 110.142 144.586 110.236C145.415 110.331 146.037 110.535 146.452 110.847C146.866 111.153 147.073 111.582 147.073 112.135C147.073 112.818 146.797 113.32 146.244 113.64C145.692 113.953 145.073 114.109 144.39 114.109ZM155.216 114.524C155.216 115.113 154.998 115.553 154.561 115.844C154.132 116.142 153.605 116.291 152.98 116.291H150.929C150.274 116.291 149.732 116.16 149.303 115.898C148.881 115.636 148.67 115.233 148.67 114.687C148.67 114.338 148.761 114.062 148.943 113.858C149.132 113.647 149.405 113.527 149.761 113.498V113.269C149.507 113.225 149.296 113.12 149.129 112.953C148.969 112.785 148.889 112.527 148.889 112.178C148.889 111.865 148.987 111.585 149.183 111.338C149.38 111.084 149.71 110.938 150.176 110.902V110.695C149.82 110.578 149.58 110.404 149.456 110.171C149.332 109.931 149.27 109.702 149.27 109.484V109.189C149.27 108.687 149.485 108.273 149.914 107.945C150.343 107.618 150.947 107.455 151.725 107.455H154.943V108.436L153.252 108.185V108.382C153.543 108.462 153.761 108.615 153.907 108.84C154.06 109.065 154.136 109.291 154.136 109.516V109.8C154.136 110.302 153.943 110.709 153.558 111.022C153.172 111.327 152.547 111.48 151.681 111.48H150.361C150.165 111.48 150.016 111.531 149.914 111.633C149.812 111.727 149.761 111.891 149.761 112.124C149.761 112.349 149.83 112.52 149.969 112.636C150.114 112.745 150.329 112.8 150.612 112.8H153.089C153.7 112.8 154.205 112.935 154.605 113.204C155.012 113.473 155.216 113.913 155.216 114.524ZM151.725 110.651C152.169 110.651 152.507 110.571 152.74 110.411C152.972 110.251 153.089 110 153.089 109.658V109.309C153.089 108.967 152.972 108.72 152.74 108.567C152.507 108.407 152.169 108.327 151.725 108.327C151.281 108.327 150.943 108.407 150.71 108.567C150.478 108.72 150.361 108.967 150.361 109.309V109.658C150.361 110 150.478 110.251 150.71 110.411C150.943 110.571 151.281 110.651 151.725 110.651ZM150.907 115.418H153.121C153.449 115.418 153.714 115.356 153.918 115.233C154.121 115.109 154.223 114.895 154.223 114.589C154.223 114.298 154.129 114.091 153.94 113.967C153.758 113.844 153.521 113.782 153.23 113.782H149.761V114.524C149.761 114.822 149.86 115.044 150.056 115.189C150.252 115.342 150.536 115.418 150.907 115.418Z" fill="var(--vscode-debugIcon-continueForeground, #9CDCFE)" /> + <path d="M118.326 91.2V90.1091H124.435V91.2H118.326ZM118.326 94.2545V93.1636H124.435V94.2545H118.326ZM131.203 116.291C130.061 116.022 129.138 115.4 128.432 114.425C127.734 113.451 127.385 112.218 127.385 110.727C127.385 109.236 127.734 108.004 128.432 107.029C129.138 106.055 130.061 105.433 131.203 105.164L131.312 106.015C130.338 106.284 129.621 106.847 129.163 107.705C128.705 108.556 128.476 109.564 128.476 110.727C128.476 111.891 128.705 112.902 129.163 113.76C129.621 114.611 130.338 115.171 131.312 115.44L131.203 116.291ZM157.402 116.291L157.293 115.44C158.267 115.171 158.984 114.607 159.442 113.749C159.9 112.884 160.129 111.876 160.129 110.727C160.129 109.578 159.9 108.575 159.442 107.716C158.984 106.851 158.267 106.284 157.293 106.015L157.402 105.164C158.544 105.433 159.464 106.058 160.162 107.04C160.867 108.015 161.22 109.244 161.22 110.727C161.22 112.211 160.867 113.44 160.162 114.415C159.464 115.396 158.544 116.022 157.402 116.291Z" fill="var(--vscode-foreground, #D4D4D4)" /> + <path d="M137.753 91.6364L137.426 87.2727H139.171L138.844 91.6364H137.753ZM134.48 91.6364L134.153 87.2727H135.898L135.571 91.6364H134.48ZM145.59 96L144.303 90.7964L143.015 96H141.924L140.746 87.2727H141.826L142.612 94.3964L143.757 89.4545H144.903L146.048 94.3964L146.833 87.2727H147.859L146.681 96H145.59ZM154.692 94.56C154.496 95.1055 154.198 95.5018 153.798 95.7491C153.398 95.9891 152.783 96.1091 151.954 96.1091C151.089 96.1091 150.416 95.9018 149.936 95.4873C149.456 95.0655 149.216 94.4436 149.216 93.6218V91.8655C149.216 91.0218 149.467 90.3927 149.969 89.9782C150.478 89.5564 151.14 89.3455 151.954 89.3455C152.776 89.3455 153.434 89.56 153.929 89.9891C154.423 90.4109 154.67 91.0618 154.67 91.9418V92.9455H150.307V93.6327C150.307 94.1782 150.441 94.5745 150.71 94.8218C150.987 95.0618 151.398 95.1818 151.943 95.1818C152.467 95.1818 152.849 95.1091 153.089 94.9636C153.329 94.8182 153.51 94.5818 153.634 94.2545L154.692 94.56ZM150.307 91.8655V92.1273H153.58V91.9636C153.58 91.3745 153.441 90.9455 153.165 90.6764C152.896 90.4073 152.489 90.2727 151.943 90.2727C151.398 90.2727 150.987 90.4036 150.71 90.6655C150.441 90.92 150.307 91.32 150.307 91.8655ZM156.857 96V95.0182H159.038V88.2545H156.857V87.2727H160.129V95.0182H162.311V96H156.857ZM167.377 96.1091C166.424 96.1091 165.704 95.9018 165.217 95.4873C164.737 95.0655 164.497 94.4473 164.497 93.6327V91.8655C164.497 91.0364 164.737 90.4109 165.217 89.9891C165.704 89.56 166.421 89.3455 167.366 89.3455C168.21 89.3455 168.857 89.5382 169.308 89.9236C169.766 90.3018 170.021 90.8582 170.072 91.5927L169.003 91.6582C168.974 91.2073 168.824 90.8727 168.555 90.6545C168.286 90.4364 167.886 90.3273 167.355 90.3273C166.759 90.3273 166.315 90.4545 166.024 90.7091C165.734 90.9564 165.588 91.3418 165.588 91.8655V93.6327C165.588 94.1345 165.734 94.5091 166.024 94.7564C166.323 95.0036 166.774 95.1273 167.377 95.1273C167.901 95.1273 168.297 95.0364 168.566 94.8545C168.835 94.6655 168.981 94.3855 169.003 94.0145L170.072 94.08C170.021 94.7491 169.77 95.2545 169.319 95.5964C168.868 95.9382 168.221 96.1091 167.377 96.1091ZM174.865 96.1091C173.985 96.1091 173.309 95.8982 172.836 95.4764C172.371 95.0473 172.138 94.4327 172.138 93.6327V91.8655C172.138 91.0582 172.374 90.4364 172.847 90C173.32 89.5636 173.996 89.3455 174.876 89.3455C175.749 89.3455 176.418 89.5636 176.883 90C177.356 90.4364 177.592 91.0582 177.592 91.8655V93.6327C177.592 94.4327 177.356 95.0473 176.883 95.4764C176.418 95.8982 175.745 96.1091 174.865 96.1091ZM174.865 95.1273C175.418 95.1273 175.829 95.0036 176.098 94.7564C176.367 94.5091 176.501 94.1345 176.501 93.6327V91.8655C176.501 91.3418 176.367 90.9564 176.098 90.7091C175.829 90.4545 175.418 90.3273 174.865 90.3273C174.312 90.3273 173.901 90.4545 173.632 90.7091C173.363 90.9564 173.229 91.3418 173.229 91.8655V93.6327C173.229 94.1345 173.363 94.5091 173.632 94.7564C173.901 95.0036 174.312 95.1273 174.865 95.1273ZM180.346 91.3091V96H179.342V89.4545H180.346V90.3273H180.564C180.702 90.0218 180.884 89.7818 181.109 89.6073C181.335 89.4327 181.571 89.3455 181.818 89.3455C182.102 89.3455 182.338 89.4327 182.527 89.6073C182.724 89.7745 182.858 90.0145 182.931 90.3273H183.171C183.309 90.0218 183.495 89.7818 183.728 89.6073C183.968 89.4327 184.222 89.3455 184.491 89.3455C184.855 89.3455 185.142 89.4945 185.353 89.7927C185.564 90.0836 185.669 90.48 185.669 90.9818V96H184.666V91.1564C184.666 90.8291 184.622 90.5818 184.535 90.4145C184.447 90.2473 184.32 90.1636 184.153 90.1636C184.015 90.1636 183.84 90.2655 183.629 90.4691C183.426 90.6727 183.218 90.9527 183.008 91.3091V96H182.004V91.1564C182.004 90.8291 181.957 90.5818 181.862 90.4145C181.775 90.2473 181.647 90.1636 181.48 90.1636C181.335 90.1636 181.164 90.2618 180.967 90.4582C180.778 90.6545 180.571 90.9382 180.346 91.3091ZM192.895 94.56C192.699 95.1055 192.401 95.5018 192.001 95.7491C191.601 95.9891 190.986 96.1091 190.157 96.1091C189.292 96.1091 188.619 95.9018 188.139 95.4873C187.659 95.0655 187.419 94.4436 187.419 93.6218V91.8655C187.419 91.0218 187.67 90.3927 188.172 89.9782C188.681 89.5564 189.343 89.3455 190.157 89.3455C190.979 89.3455 191.637 89.56 192.132 89.9891C192.626 90.4109 192.874 91.0618 192.874 91.9418V92.9455H188.51V93.6327C188.51 94.1782 188.644 94.5745 188.914 94.8218C189.19 95.0618 189.601 95.1818 190.146 95.1818C190.67 95.1818 191.052 95.1091 191.292 94.9636C191.532 94.8182 191.714 94.5818 191.837 94.2545L192.895 94.56ZM188.51 91.8655V92.1273H191.783V91.9636C191.783 91.3745 191.644 90.9455 191.368 90.6764C191.099 90.4073 190.692 90.2727 190.146 90.2727C189.601 90.2727 189.19 90.4036 188.914 90.6655C188.644 90.92 188.51 91.32 188.51 91.8655ZM206.508 96.0982C205.788 96.0982 205.228 95.92 204.828 95.5636C204.428 95.2073 204.228 94.6618 204.228 93.9273V90.4364H202.373V89.4545H204.228V87.2727H205.318V89.4545H208.155V90.4364H205.318V93.8291C205.318 94.2509 205.424 94.5636 205.635 94.7673C205.846 94.9709 206.158 95.0727 206.573 95.0727C206.849 95.0727 207.108 95.0545 207.348 95.0182C207.588 94.9745 207.817 94.92 208.035 94.8545L208.253 95.8473C208.006 95.92 207.74 95.9782 207.457 96.0218C207.18 96.0727 206.864 96.0982 206.508 96.0982ZM213.068 96.1091C212.188 96.1091 211.512 95.8982 211.039 95.4764C210.574 95.0473 210.341 94.4327 210.341 93.6327V91.8655C210.341 91.0582 210.577 90.4364 211.05 90C211.523 89.5636 212.199 89.3455 213.079 89.3455C213.952 89.3455 214.621 89.5636 215.086 90C215.559 90.4364 215.795 91.0582 215.795 91.8655V93.6327C215.795 94.4327 215.559 95.0473 215.086 95.4764C214.621 95.8982 213.948 96.1091 213.068 96.1091ZM213.068 95.1273C213.621 95.1273 214.032 95.0036 214.301 94.7564C214.57 94.5091 214.705 94.1345 214.705 93.6327V91.8655C214.705 91.3418 214.57 90.9564 214.301 90.7091C214.032 90.4545 213.621 90.3273 213.068 90.3273C212.515 90.3273 212.105 90.4545 211.835 90.7091C211.566 90.9564 211.432 91.3418 211.432 91.8655V93.6327C211.432 94.1345 211.566 94.5091 211.835 94.7564C212.105 95.0036 212.515 95.1273 213.068 95.1273ZM229.517 96L226.604 88.6473V96H225.622V87.2727H227.117L230.095 94.7891V87.2727H231.077V96H229.517ZM235.99 96.1091C235.11 96.1091 234.434 95.8982 233.961 95.4764C233.496 95.0473 233.263 94.4327 233.263 93.6327V91.8655C233.263 91.0582 233.499 90.4364 233.972 90C234.445 89.5636 235.121 89.3455 236.001 89.3455C236.874 89.3455 237.543 89.5636 238.008 90C238.481 90.4364 238.717 91.0582 238.717 91.8655V93.6327C238.717 94.4327 238.481 95.0473 238.008 95.4764C237.543 95.8982 236.87 96.1091 235.99 96.1091ZM235.99 95.1273C236.543 95.1273 236.954 95.0036 237.223 94.7564C237.492 94.5091 237.626 94.1345 237.626 93.6327V91.8655C237.626 91.3418 237.492 90.9564 237.223 90.7091C236.954 90.4545 236.543 90.3273 235.99 90.3273C235.437 90.3273 235.026 90.4545 234.757 90.7091C234.488 90.9564 234.354 91.3418 234.354 91.8655V93.6327C234.354 94.1345 234.488 94.5091 234.757 94.7564C235.026 95.0036 235.437 95.1273 235.99 95.1273ZM244.711 96.0982C243.991 96.0982 243.431 95.92 243.031 95.5636C242.631 95.2073 242.431 94.6618 242.431 93.9273V90.4364H240.576V89.4545H242.431V87.2727H243.522V89.4545H246.358V90.4364H243.522V93.8291C243.522 94.2509 243.627 94.5636 243.838 94.7673C244.049 94.9709 244.362 95.0727 244.776 95.0727C245.052 95.0727 245.311 95.0545 245.551 95.0182C245.791 94.9745 246.02 94.92 246.238 94.8545L246.456 95.8473C246.209 95.92 245.943 95.9782 245.66 96.0218C245.383 96.0727 245.067 96.0982 244.711 96.0982ZM254.02 94.56C253.824 95.1055 253.526 95.5018 253.126 95.7491C252.726 95.9891 252.111 96.1091 251.282 96.1091C250.417 96.1091 249.744 95.9018 249.264 95.4873C248.784 95.0655 248.544 94.4436 248.544 93.6218V91.8655C248.544 91.0218 248.795 90.3927 249.297 89.9782C249.806 89.5564 250.468 89.3455 251.282 89.3455C252.104 89.3455 252.762 89.56 253.257 89.9891C253.751 90.4109 253.999 91.0618 253.999 91.9418V92.9455H249.635V93.6327C249.635 94.1782 249.769 94.5745 250.039 94.8218C250.315 95.0618 250.726 95.1818 251.271 95.1818C251.795 95.1818 252.177 95.1091 252.417 94.9636C252.657 94.8182 252.839 94.5818 252.962 94.2545L254.02 94.56ZM249.635 91.8655V92.1273H252.908V91.9636C252.908 91.3745 252.769 90.9455 252.493 90.6764C252.224 90.4073 251.817 90.2727 251.271 90.2727C250.726 90.2727 250.315 90.4036 250.039 90.6655C249.769 90.92 249.635 91.32 249.635 91.8655ZM258.825 96.1091C258.388 96.1091 257.937 96.0436 257.472 95.9127C257.006 95.7818 256.577 95.5927 256.185 95.3455V87.2727H257.276V90.5455H257.603C257.792 90.1673 258.054 89.8727 258.388 89.6618C258.723 89.4509 259.097 89.3455 259.512 89.3455C260.188 89.3455 260.712 89.5636 261.083 90C261.454 90.4364 261.639 91.0473 261.639 91.8327V93.6109C261.639 94.4036 261.392 95.0182 260.897 95.4545C260.403 95.8909 259.712 96.1091 258.825 96.1091ZM258.814 95.1491C259.403 95.1491 259.839 95.0218 260.123 94.7673C260.406 94.5055 260.548 94.1127 260.548 93.5891V91.8436C260.548 91.3491 260.439 90.9818 260.221 90.7418C260.003 90.5018 259.676 90.3818 259.239 90.3818C258.883 90.3818 258.545 90.4764 258.225 90.6655C257.905 90.8545 257.588 91.1418 257.276 91.5273V94.7345C257.508 94.8727 257.748 94.9782 257.996 95.0509C258.25 95.1164 258.523 95.1491 258.814 95.1491ZM266.553 96.1091C265.673 96.1091 264.996 95.8982 264.523 95.4764C264.058 95.0473 263.825 94.4327 263.825 93.6327V91.8655C263.825 91.0582 264.062 90.4364 264.534 90C265.007 89.5636 265.683 89.3455 266.563 89.3455C267.436 89.3455 268.105 89.5636 268.571 90C269.043 90.4364 269.28 91.0582 269.28 91.8655V93.6327C269.28 94.4327 269.043 95.0473 268.571 95.4764C268.105 95.8982 267.433 96.1091 266.553 96.1091ZM266.553 95.1273C267.105 95.1273 267.516 95.0036 267.785 94.7564C268.054 94.5091 268.189 94.1345 268.189 93.6327V91.8655C268.189 91.3418 268.054 90.9564 267.785 90.7091C267.516 90.4545 267.105 90.3273 266.553 90.3273C266 90.3273 265.589 90.4545 265.32 90.7091C265.051 90.9564 264.916 91.3418 264.916 91.8655V93.6327C264.916 94.1345 265.051 94.5091 265.32 94.7564C265.589 95.0036 266 95.1273 266.553 95.1273ZM274.193 96.1091C273.313 96.1091 272.637 95.8982 272.164 95.4764C271.699 95.0473 271.466 94.4327 271.466 93.6327V91.8655C271.466 91.0582 271.702 90.4364 272.175 90C272.648 89.5636 273.324 89.3455 274.204 89.3455C275.077 89.3455 275.746 89.5636 276.211 90C276.684 90.4364 276.92 91.0582 276.92 91.8655V93.6327C276.92 94.4327 276.684 95.0473 276.211 95.4764C275.746 95.8982 275.073 96.1091 274.193 96.1091ZM274.193 95.1273C274.746 95.1273 275.157 95.0036 275.426 94.7564C275.695 94.5091 275.83 94.1345 275.83 93.6327V91.8655C275.83 91.3418 275.695 90.9564 275.426 90.7091C275.157 90.4545 274.746 90.3273 274.193 90.3273C273.64 90.3273 273.23 90.4545 272.96 90.7091C272.691 90.9564 272.557 91.3418 272.557 91.8655V93.6327C272.557 94.1345 272.691 94.5091 272.96 94.7564C273.23 95.0036 273.64 95.1273 274.193 95.1273ZM280.743 93.0218V96H279.652V87.2727H280.743V92.1055H281.299L283.394 89.4545H284.725L282.161 92.4982L284.692 96H283.448L281.299 93.0218H280.743ZM289.562 96.1091C289.009 96.1091 288.474 96.0291 287.958 95.8691C287.442 95.7018 286.965 95.4473 286.529 95.1055L287.02 94.2436C287.376 94.5345 287.773 94.7564 288.209 94.9091C288.653 95.0545 289.104 95.1273 289.562 95.1273C289.94 95.1273 290.3 95.0691 290.642 94.9527C290.991 94.8364 291.165 94.5891 291.165 94.2109C291.165 93.8982 291.024 93.6764 290.74 93.5455C290.456 93.4145 289.936 93.3018 289.18 93.2073C288.38 93.1127 287.773 92.9273 287.358 92.6509C286.951 92.3673 286.747 91.9236 286.747 91.32C286.747 90.6655 286.994 90.1745 287.489 89.8473C287.984 89.5127 288.558 89.3455 289.213 89.3455C289.729 89.3455 290.224 89.4327 290.696 89.6073C291.176 89.7818 291.62 90.0291 292.027 90.3491L291.536 91.2109C291.202 90.9418 290.838 90.7273 290.445 90.5673C290.053 90.4073 289.642 90.3273 289.213 90.3273C288.864 90.3273 288.544 90.3927 288.253 90.5236C287.969 90.6545 287.827 90.8945 287.827 91.2436C287.827 91.5418 287.958 91.76 288.22 91.8982C288.489 92.0291 289.002 92.1418 289.758 92.2364C290.587 92.3309 291.209 92.5345 291.624 92.8473C292.038 93.1527 292.245 93.5818 292.245 94.1345C292.245 94.8182 291.969 95.32 291.416 95.64C290.864 95.9527 290.245 96.1091 289.562 96.1091ZM302.028 96V95.0182H304.21V90.4364H302.028V89.4545H305.301V95.0182H307.483V96H302.028ZM304.756 88.2545C304.479 88.2545 304.254 88.2109 304.079 88.1236C303.912 88.0364 303.828 87.8727 303.828 87.6327V87.5564C303.828 87.3091 303.912 87.1418 304.079 87.0545C304.254 86.96 304.479 86.9127 304.756 86.9127C305.032 86.9127 305.254 86.96 305.421 87.0545C305.596 87.1418 305.683 87.3091 305.683 87.5564V87.6327C305.683 87.8727 305.596 88.0364 305.421 88.1236C305.247 88.2109 305.025 88.2545 304.756 88.2545ZM309.669 96V89.4545H310.76V90.5455H311.087C311.247 90.1745 311.498 89.8836 311.84 89.6727C312.182 89.4545 312.571 89.3455 313.007 89.3455C313.662 89.3455 314.178 89.5455 314.556 89.9455C314.934 90.3455 315.124 90.8909 315.124 91.5818V96H314.033V91.8C314.033 91.3418 313.92 90.9927 313.694 90.7527C313.476 90.5055 313.16 90.3818 312.745 90.3818C312.418 90.3818 312.084 90.48 311.742 90.6764C311.407 90.8727 311.08 91.1564 310.76 91.5273V96H309.669ZM328.256 96H327.11L324.296 87.2727H325.474L327.699 94.6145L329.925 87.2727H331.07L328.256 96ZM335.46 96.0655C334.805 96.0655 334.195 95.96 333.627 95.7491C333.06 95.5382 332.551 95.1527 332.1 94.5927L332.885 93.8509C333.191 94.2655 333.555 94.5745 333.976 94.7782C334.405 94.9818 334.911 95.0836 335.493 95.0836C336.031 95.0836 336.464 94.96 336.791 94.7127C337.125 94.4582 337.293 94.1164 337.293 93.6873C337.293 93.2727 337.151 92.9564 336.867 92.7382C336.584 92.5127 336.024 92.2764 335.187 92.0291C334.125 91.7236 333.402 91.3927 333.016 91.0364C332.631 90.68 332.438 90.1745 332.438 89.52C332.438 88.7636 332.689 88.1927 333.191 87.8073C333.7 87.4218 334.315 87.2291 335.035 87.2291C335.645 87.2291 336.224 87.3382 336.769 87.5564C337.322 87.7673 337.809 88.1455 338.231 88.6909L337.478 89.4436C337.187 89.0291 336.824 88.72 336.387 88.5164C335.951 88.3127 335.507 88.2109 335.056 88.2109C334.598 88.2109 334.231 88.3055 333.955 88.4945C333.678 88.6764 333.54 88.9855 333.54 89.4218C333.54 89.8073 333.689 90.1127 333.987 90.3382C334.293 90.5564 334.889 90.7927 335.776 91.0473C336.78 91.3382 337.464 91.6582 337.827 92.0073C338.198 92.3491 338.384 92.8473 338.384 93.5018C338.384 94.3382 338.085 94.9745 337.489 95.4109C336.893 95.8473 336.216 96.0655 335.46 96.0655ZM350.709 96.1091C349.69 96.1091 348.927 95.8509 348.418 95.3345C347.909 94.8109 347.654 94.0255 347.654 92.9782V90.3382C347.654 89.2764 347.909 88.4836 348.418 87.96C348.927 87.4291 349.694 87.1636 350.719 87.1636C351.723 87.1636 352.465 87.4 352.945 87.8727C353.432 88.3382 353.705 89.0873 353.763 90.12L352.672 90.2291C352.599 89.4436 352.421 88.9018 352.138 88.6036C351.861 88.2982 351.385 88.1455 350.709 88.1455C349.989 88.1455 349.479 88.3127 349.181 88.6473C348.89 88.9745 348.745 89.5382 348.745 90.3382V92.9782C348.745 93.7636 348.89 94.32 349.181 94.6473C349.479 94.9673 349.989 95.1273 350.709 95.1273C351.385 95.1273 351.861 94.9818 352.138 94.6909C352.421 94.3927 352.599 93.8582 352.672 93.0873L353.763 93.1964C353.705 94.2145 353.432 94.9564 352.945 95.4218C352.465 95.88 351.719 96.1091 350.709 96.1091ZM358.24 96.1091C357.36 96.1091 356.684 95.8982 356.211 95.4764C355.746 95.0473 355.513 94.4327 355.513 93.6327V91.8655C355.513 91.0582 355.749 90.4364 356.222 90C356.695 89.5636 357.371 89.3455 358.251 89.3455C359.124 89.3455 359.793 89.5636 360.258 90C360.731 90.4364 360.967 91.0582 360.967 91.8655V93.6327C360.967 94.4327 360.731 95.0473 360.258 95.4764C359.793 95.8982 359.12 96.1091 358.24 96.1091ZM358.24 95.1273C358.793 95.1273 359.204 95.0036 359.473 94.7564C359.742 94.5091 359.876 94.1345 359.876 93.6327V91.8655C359.876 91.3418 359.742 90.9564 359.473 90.7091C359.204 90.4545 358.793 90.3273 358.24 90.3273C357.687 90.3273 357.276 90.4545 357.007 90.7091C356.738 90.9564 356.604 91.3418 356.604 91.8655V93.6327C356.604 94.1345 356.738 94.5091 357.007 94.7564C357.276 95.0036 357.687 95.1273 358.24 95.1273ZM365.553 95.0727C365.91 95.0727 366.248 94.9782 366.568 94.7891C366.888 94.6 367.204 94.3127 367.517 93.9273V90.72C367.292 90.5818 367.052 90.48 366.797 90.4145C366.543 90.3418 366.27 90.3055 365.979 90.3055C365.39 90.3055 364.953 90.4364 364.67 90.6982C364.386 90.9527 364.244 91.3418 364.244 91.8655V93.6218C364.244 94.1091 364.353 94.4727 364.572 94.7127C364.79 94.9527 365.117 95.0727 365.553 95.0727ZM367.19 94.9091C367.001 95.2873 366.739 95.5818 366.404 95.7927C366.07 96.0036 365.695 96.1091 365.281 96.1091C364.604 96.1091 364.081 95.8909 363.71 95.4545C363.339 95.0182 363.153 94.4073 363.153 93.6218V91.8436C363.153 91.0509 363.401 90.4364 363.895 90C364.39 89.5636 365.081 89.3455 365.968 89.3455C366.223 89.3455 366.484 89.3745 366.753 89.4327C367.023 89.4836 367.277 89.5564 367.517 89.6509V87.2727H368.608V96H367.517V94.9091H367.19ZM376.27 94.56C376.074 95.1055 375.776 95.5018 375.376 95.7491C374.976 95.9891 374.361 96.1091 373.532 96.1091C372.667 96.1091 371.994 95.9018 371.514 95.4873C371.034 95.0655 370.794 94.4436 370.794 93.6218V91.8655C370.794 91.0218 371.045 90.3927 371.547 89.9782C372.056 89.5564 372.718 89.3455 373.532 89.3455C374.354 89.3455 375.012 89.56 375.507 89.9891C376.001 90.4109 376.249 91.0618 376.249 91.9418V92.9455H371.885V93.6327C371.885 94.1782 372.019 94.5745 372.289 94.8218C372.565 95.0618 372.976 95.1818 373.521 95.1818C374.045 95.1818 374.427 95.1091 374.667 94.9636C374.907 94.8182 375.089 94.5818 375.212 94.2545L376.27 94.56ZM371.885 91.8655V92.1273H375.158V91.9636C375.158 91.3745 375.019 90.9455 374.743 90.6764C374.474 90.4073 374.067 90.2727 373.521 90.2727C372.976 90.2727 372.565 90.4036 372.289 90.6655C372.019 90.92 371.885 91.32 371.885 91.8655ZM380.726 92.7273L380.398 87.2727H381.926L381.598 92.7273H380.726ZM381.173 96.1091C380.809 96.1091 380.54 96.0364 380.366 95.8909C380.198 95.7382 380.115 95.5055 380.115 95.1927V94.8873C380.115 94.5673 380.198 94.3309 380.366 94.1782C380.54 94.0255 380.809 93.9491 381.173 93.9491C381.536 93.9491 381.802 94.0255 381.969 94.1782C382.136 94.3309 382.22 94.5673 382.22 94.8873V95.1927C382.22 95.5055 382.133 95.7382 381.958 95.8909C381.791 96.0364 381.529 96.1091 381.173 96.1091ZM389.893 91.6364L389.566 87.2727H391.312L390.984 91.6364H389.893ZM386.621 91.6364L386.293 87.2727H388.039L387.712 91.6364H386.621Z" fill="var(--vscode-debugTokenExpression-string, #CE9178)" /> + <path d="M90.72 113.149C91.3309 113.149 91.7709 113.025 92.04 112.778C92.3164 112.531 92.4545 112.135 92.4545 111.589V109.822C92.4545 109.32 92.3491 108.956 92.1382 108.731C91.9345 108.498 91.6036 108.382 91.1455 108.382C90.7818 108.382 90.4436 108.473 90.1309 108.655C89.8182 108.836 89.5018 109.127 89.1818 109.527V112.735C89.4073 112.873 89.64 112.978 89.88 113.051C90.1273 113.116 90.4073 113.149 90.72 113.149ZM90.7309 114.109C90.4836 114.109 90.2218 114.08 89.9455 114.022C89.6764 113.971 89.4218 113.898 89.1818 113.804V116.182H88.0909V107.455H89.1818V108.545H89.5091C89.6909 108.175 89.9527 107.884 90.2945 107.673C90.6436 107.455 91.0182 107.345 91.4182 107.345C92.0727 107.345 92.5891 107.571 92.9673 108.022C93.3527 108.465 93.5455 109.069 93.5455 109.833V111.611C93.5455 112.382 93.2909 112.993 92.7818 113.444C92.28 113.887 91.5964 114.109 90.7309 114.109ZM96.277 114V107.455H97.3679V108.545H97.6952C97.8479 108.131 98.0806 107.829 98.3934 107.64C98.7061 107.444 99.0843 107.345 99.5279 107.345C100.015 107.345 100.422 107.509 100.75 107.836C101.077 108.156 101.241 108.611 101.241 109.2V110.182H100.204V109.593C100.204 109.178 100.11 108.873 99.9206 108.676C99.7388 108.48 99.4588 108.382 99.0806 108.382C98.7388 108.382 98.4261 108.465 98.1424 108.633C97.8661 108.8 97.6079 109.062 97.3679 109.418V114H96.277ZM103.372 114V113.018H105.554V108.436H103.372V107.455H106.645V113.018H108.827V114H103.372ZM106.099 106.255C105.823 106.255 105.598 106.211 105.423 106.124C105.256 106.036 105.172 105.873 105.172 105.633V105.556C105.172 105.309 105.256 105.142 105.423 105.055C105.598 104.96 105.823 104.913 106.099 104.913C106.376 104.913 106.598 104.96 106.765 105.055C106.939 105.142 107.027 105.309 107.027 105.556V105.633C107.027 105.873 106.939 106.036 106.765 106.124C106.59 106.211 106.369 106.255 106.099 106.255ZM111.013 114V107.455H112.104V108.545H112.431C112.591 108.175 112.842 107.884 113.184 107.673C113.526 107.455 113.915 107.345 114.351 107.345C115.006 107.345 115.522 107.545 115.9 107.945C116.278 108.345 116.467 108.891 116.467 109.582V114H115.376V109.8C115.376 109.342 115.264 108.993 115.038 108.753C114.82 108.505 114.504 108.382 114.089 108.382C113.762 108.382 113.427 108.48 113.086 108.676C112.751 108.873 112.424 109.156 112.104 109.527V114H111.013ZM122.461 114.098C121.741 114.098 121.181 113.92 120.781 113.564C120.381 113.207 120.181 112.662 120.181 111.927V108.436H118.326V107.455H120.181V105.273H121.272V107.455H124.108V108.436H121.272V111.829C121.272 112.251 121.377 112.564 121.588 112.767C121.799 112.971 122.112 113.073 122.526 113.073C122.802 113.073 123.061 113.055 123.301 113.018C123.541 112.975 123.77 112.92 123.988 112.855L124.206 113.847C123.959 113.92 123.693 113.978 123.41 114.022C123.133 114.073 122.817 114.098 122.461 114.098Z" fill="var(--vscode-debugConsole-warningForeground, #DCDCAA)" /> + <path d="M80.1055 156L78.8182 150.796L77.5309 156H76.44L75.2618 147.273H76.3418L77.1273 154.396L78.2727 149.455H79.4182L80.5636 154.396L81.3491 147.273H82.3745L81.1964 156H80.1055ZM89.2079 154.56C89.0115 155.105 88.7134 155.502 88.3134 155.749C87.9134 155.989 87.2988 156.109 86.4697 156.109C85.6043 156.109 84.9315 155.902 84.4515 155.487C83.9715 155.065 83.7315 154.444 83.7315 153.622V151.865C83.7315 151.022 83.9824 150.393 84.4843 149.978C84.9934 149.556 85.6552 149.345 86.4697 149.345C87.2915 149.345 87.9497 149.56 88.4443 149.989C88.9388 150.411 89.1861 151.062 89.1861 151.942V152.945H84.8224V153.633C84.8224 154.178 84.957 154.575 85.2261 154.822C85.5024 155.062 85.9134 155.182 86.4588 155.182C86.9824 155.182 87.3643 155.109 87.6043 154.964C87.8443 154.818 88.0261 154.582 88.1497 154.255L89.2079 154.56ZM84.8224 151.865V152.127H88.0952V151.964C88.0952 151.375 87.957 150.945 87.6806 150.676C87.4115 150.407 87.0043 150.273 86.4588 150.273C85.9134 150.273 85.5024 150.404 85.2261 150.665C84.957 150.92 84.8224 151.32 84.8224 151.865ZM91.3722 156V155.018H93.554V148.255H91.3722V147.273H94.6449V155.018H96.8267V156H91.3722ZM101.893 156.109C100.94 156.109 100.22 155.902 99.7328 155.487C99.2528 155.065 99.0128 154.447 99.0128 153.633V151.865C99.0128 151.036 99.2528 150.411 99.7328 149.989C100.22 149.56 100.936 149.345 101.882 149.345C102.726 149.345 103.373 149.538 103.824 149.924C104.282 150.302 104.536 150.858 104.587 151.593L103.518 151.658C103.489 151.207 103.34 150.873 103.071 150.655C102.802 150.436 102.402 150.327 101.871 150.327C101.275 150.327 100.831 150.455 100.54 150.709C100.249 150.956 100.104 151.342 100.104 151.865V153.633C100.104 154.135 100.249 154.509 100.54 154.756C100.838 155.004 101.289 155.127 101.893 155.127C102.416 155.127 102.813 155.036 103.082 154.855C103.351 154.665 103.496 154.385 103.518 154.015L104.587 154.08C104.536 154.749 104.286 155.255 103.835 155.596C103.384 155.938 102.736 156.109 101.893 156.109ZM109.381 156.109C108.501 156.109 107.824 155.898 107.352 155.476C106.886 155.047 106.653 154.433 106.653 153.633V151.865C106.653 151.058 106.89 150.436 107.363 150C107.835 149.564 108.512 149.345 109.392 149.345C110.264 149.345 110.933 149.564 111.399 150C111.872 150.436 112.108 151.058 112.108 151.865V153.633C112.108 154.433 111.872 155.047 111.399 155.476C110.933 155.898 110.261 156.109 109.381 156.109ZM109.381 155.127C109.933 155.127 110.344 155.004 110.613 154.756C110.883 154.509 111.017 154.135 111.017 153.633V151.865C111.017 151.342 110.883 150.956 110.613 150.709C110.344 150.455 109.933 150.327 109.381 150.327C108.828 150.327 108.417 150.455 108.148 150.709C107.879 150.956 107.744 151.342 107.744 151.865V153.633C107.744 154.135 107.879 154.509 108.148 154.756C108.417 155.004 108.828 155.127 109.381 155.127ZM114.861 151.309V156H113.858V149.455H114.861V150.327H115.079C115.218 150.022 115.399 149.782 115.625 149.607C115.85 149.433 116.087 149.345 116.334 149.345C116.618 149.345 116.854 149.433 117.043 149.607C117.239 149.775 117.374 150.015 117.447 150.327H117.687C117.825 150.022 118.01 149.782 118.243 149.607C118.483 149.433 118.738 149.345 119.007 149.345C119.37 149.345 119.658 149.495 119.869 149.793C120.079 150.084 120.185 150.48 120.185 150.982V156H119.181V151.156C119.181 150.829 119.138 150.582 119.05 150.415C118.963 150.247 118.836 150.164 118.669 150.164C118.53 150.164 118.356 150.265 118.145 150.469C117.941 150.673 117.734 150.953 117.523 151.309V156H116.519V151.156C116.519 150.829 116.472 150.582 116.378 150.415C116.29 150.247 116.163 150.164 115.996 150.164C115.85 150.164 115.679 150.262 115.483 150.458C115.294 150.655 115.087 150.938 114.861 151.309ZM127.411 154.56C127.215 155.105 126.916 155.502 126.516 155.749C126.116 155.989 125.502 156.109 124.673 156.109C123.807 156.109 123.135 155.902 122.655 155.487C122.175 155.065 121.935 154.444 121.935 153.622V151.865C121.935 151.022 122.186 150.393 122.687 149.978C123.196 149.556 123.858 149.345 124.673 149.345C125.495 149.345 126.153 149.56 126.647 149.989C127.142 150.411 127.389 151.062 127.389 151.942V152.945H123.026V153.633C123.026 154.178 123.16 154.575 123.429 154.822C123.706 155.062 124.116 155.182 124.662 155.182C125.186 155.182 125.567 155.109 125.807 154.964C126.047 154.818 126.229 154.582 126.353 154.255L127.411 154.56ZM123.026 151.865V152.127H126.298V151.964C126.298 151.375 126.16 150.945 125.884 150.676C125.615 150.407 125.207 150.273 124.662 150.273C124.116 150.273 123.706 150.404 123.429 150.665C123.16 150.92 123.026 151.32 123.026 151.865ZM141.023 156.098C140.303 156.098 139.743 155.92 139.343 155.564C138.943 155.207 138.743 154.662 138.743 153.927V150.436H136.889V149.455H138.743V147.273H139.834V149.455H142.67V150.436H139.834V153.829C139.834 154.251 139.94 154.564 140.15 154.767C140.361 154.971 140.674 155.073 141.089 155.073C141.365 155.073 141.623 155.055 141.863 155.018C142.103 154.975 142.332 154.92 142.55 154.855L142.769 155.847C142.521 155.92 142.256 155.978 141.972 156.022C141.696 156.073 141.38 156.098 141.023 156.098ZM147.584 156.109C146.704 156.109 146.027 155.898 145.555 155.476C145.089 155.047 144.857 154.433 144.857 153.633V151.865C144.857 151.058 145.093 150.436 145.566 150C146.038 149.564 146.715 149.345 147.595 149.345C148.467 149.345 149.137 149.564 149.602 150C150.075 150.436 150.311 151.058 150.311 151.865V153.633C150.311 154.433 150.075 155.047 149.602 155.476C149.137 155.898 148.464 156.109 147.584 156.109ZM147.584 155.127C148.137 155.127 148.547 155.004 148.817 154.756C149.086 154.509 149.22 154.135 149.22 153.633V151.865C149.22 151.342 149.086 150.956 148.817 150.709C148.547 150.455 148.137 150.327 147.584 150.327C147.031 150.327 146.62 150.455 146.351 150.709C146.082 150.956 145.947 151.342 145.947 151.865V153.633C145.947 154.135 146.082 154.509 146.351 154.756C146.62 155.004 147.031 155.127 147.584 155.127ZM164.032 156L161.12 148.647V156H160.138V147.273H161.632L164.611 154.789V147.273H165.592V156H164.032ZM170.506 156.109C169.626 156.109 168.949 155.898 168.477 155.476C168.011 155.047 167.778 154.433 167.778 153.633V151.865C167.778 151.058 168.015 150.436 168.488 150C168.96 149.564 169.637 149.345 170.517 149.345C171.389 149.345 172.058 149.564 172.524 150C172.997 150.436 173.233 151.058 173.233 151.865V153.633C173.233 154.433 172.997 155.047 172.524 155.476C172.058 155.898 171.386 156.109 170.506 156.109ZM170.506 155.127C171.058 155.127 171.469 155.004 171.738 154.756C172.008 154.509 172.142 154.135 172.142 153.633V151.865C172.142 151.342 172.008 150.956 171.738 150.709C171.469 150.455 171.058 150.327 170.506 150.327C169.953 150.327 169.542 150.455 169.273 150.709C169.004 150.956 168.869 151.342 168.869 151.865V153.633C168.869 154.135 169.004 154.509 169.273 154.756C169.542 155.004 169.953 155.127 170.506 155.127ZM179.226 156.098C178.506 156.098 177.946 155.92 177.546 155.564C177.146 155.207 176.946 154.662 176.946 153.927V150.436H175.092V149.455H176.946V147.273H178.037V149.455H180.874V150.436H178.037V153.829C178.037 154.251 178.143 154.564 178.354 154.767C178.564 154.971 178.877 155.073 179.292 155.073C179.568 155.073 179.826 155.055 180.066 155.018C180.306 154.975 180.535 154.92 180.754 154.855L180.972 155.847C180.724 155.92 180.459 155.978 180.175 156.022C179.899 156.073 179.583 156.098 179.226 156.098ZM188.536 154.56C188.34 155.105 188.041 155.502 187.641 155.749C187.241 155.989 186.627 156.109 185.798 156.109C184.932 156.109 184.26 155.902 183.78 155.487C183.3 155.065 183.06 154.444 183.06 153.622V151.865C183.06 151.022 183.311 150.393 183.812 149.978C184.321 149.556 184.983 149.345 185.798 149.345C186.62 149.345 187.278 149.56 187.772 149.989C188.267 150.411 188.514 151.062 188.514 151.942V152.945H184.151V153.633C184.151 154.178 184.285 154.575 184.554 154.822C184.831 155.062 185.241 155.182 185.787 155.182C186.311 155.182 186.692 155.109 186.932 154.964C187.172 154.818 187.354 154.582 187.478 154.255L188.536 154.56ZM184.151 151.865V152.127H187.423V151.964C187.423 151.375 187.285 150.945 187.009 150.676C186.74 150.407 186.332 150.273 185.787 150.273C185.241 150.273 184.831 150.404 184.554 150.665C184.285 150.92 184.151 151.32 184.151 151.865ZM193.34 156.109C192.904 156.109 192.453 156.044 191.988 155.913C191.522 155.782 191.093 155.593 190.7 155.345V147.273H191.791V150.545H192.118C192.308 150.167 192.569 149.873 192.904 149.662C193.238 149.451 193.613 149.345 194.028 149.345C194.704 149.345 195.228 149.564 195.598 150C195.969 150.436 196.155 151.047 196.155 151.833V153.611C196.155 154.404 195.908 155.018 195.413 155.455C194.918 155.891 194.228 156.109 193.34 156.109ZM193.329 155.149C193.918 155.149 194.355 155.022 194.638 154.767C194.922 154.505 195.064 154.113 195.064 153.589V151.844C195.064 151.349 194.955 150.982 194.737 150.742C194.518 150.502 194.191 150.382 193.755 150.382C193.398 150.382 193.06 150.476 192.74 150.665C192.42 150.855 192.104 151.142 191.791 151.527V154.735C192.024 154.873 192.264 154.978 192.511 155.051C192.766 155.116 193.038 155.149 193.329 155.149ZM201.068 156.109C200.188 156.109 199.512 155.898 199.039 155.476C198.574 155.047 198.341 154.433 198.341 153.633V151.865C198.341 151.058 198.577 150.436 199.05 150C199.523 149.564 200.199 149.345 201.079 149.345C201.952 149.345 202.621 149.564 203.086 150C203.559 150.436 203.795 151.058 203.795 151.865V153.633C203.795 154.433 203.559 155.047 203.086 155.476C202.621 155.898 201.948 156.109 201.068 156.109ZM201.068 155.127C201.621 155.127 202.032 155.004 202.301 154.756C202.57 154.509 202.705 154.135 202.705 153.633V151.865C202.705 151.342 202.57 150.956 202.301 150.709C202.032 150.455 201.621 150.327 201.068 150.327C200.515 150.327 200.105 150.455 199.835 150.709C199.566 150.956 199.432 151.342 199.432 151.865V153.633C199.432 154.135 199.566 154.509 199.835 154.756C200.105 155.004 200.515 155.127 201.068 155.127ZM208.709 156.109C207.829 156.109 207.152 155.898 206.68 155.476C206.214 155.047 205.982 154.433 205.982 153.633V151.865C205.982 151.058 206.218 150.436 206.691 150C207.163 149.564 207.84 149.345 208.72 149.345C209.592 149.345 210.262 149.564 210.727 150C211.2 150.436 211.436 151.058 211.436 151.865V153.633C211.436 154.433 211.2 155.047 210.727 155.476C210.262 155.898 209.589 156.109 208.709 156.109ZM208.709 155.127C209.262 155.127 209.672 155.004 209.942 154.756C210.211 154.509 210.345 154.135 210.345 153.633V151.865C210.345 151.342 210.211 150.956 209.942 150.709C209.672 150.455 209.262 150.327 208.709 150.327C208.156 150.327 207.745 150.455 207.476 150.709C207.207 150.956 207.072 151.342 207.072 151.865V153.633C207.072 154.135 207.207 154.509 207.476 154.756C207.745 155.004 208.156 155.127 208.709 155.127ZM215.259 153.022V156H214.168V147.273H215.259V152.105H215.815L217.909 149.455H219.24L216.677 152.498L219.208 156H217.964L215.815 153.022H215.259ZM224.077 156.109C223.525 156.109 222.99 156.029 222.474 155.869C221.957 155.702 221.481 155.447 221.045 155.105L221.536 154.244C221.892 154.535 222.288 154.756 222.725 154.909C223.168 155.055 223.619 155.127 224.077 155.127C224.456 155.127 224.816 155.069 225.157 154.953C225.506 154.836 225.681 154.589 225.681 154.211C225.681 153.898 225.539 153.676 225.256 153.545C224.972 153.415 224.452 153.302 223.696 153.207C222.896 153.113 222.288 152.927 221.874 152.651C221.466 152.367 221.263 151.924 221.263 151.32C221.263 150.665 221.51 150.175 222.005 149.847C222.499 149.513 223.074 149.345 223.728 149.345C224.245 149.345 224.739 149.433 225.212 149.607C225.692 149.782 226.136 150.029 226.543 150.349L226.052 151.211C225.717 150.942 225.354 150.727 224.961 150.567C224.568 150.407 224.157 150.327 223.728 150.327C223.379 150.327 223.059 150.393 222.768 150.524C222.485 150.655 222.343 150.895 222.343 151.244C222.343 151.542 222.474 151.76 222.736 151.898C223.005 152.029 223.517 152.142 224.274 152.236C225.103 152.331 225.725 152.535 226.139 152.847C226.554 153.153 226.761 153.582 226.761 154.135C226.761 154.818 226.485 155.32 225.932 155.64C225.379 155.953 224.761 156.109 224.077 156.109ZM236.544 156V155.018H238.726V150.436H236.544V149.455H239.817V155.018H241.999V156H236.544ZM239.271 148.255C238.995 148.255 238.769 148.211 238.595 148.124C238.428 148.036 238.344 147.873 238.344 147.633V147.556C238.344 147.309 238.428 147.142 238.595 147.055C238.769 146.96 238.995 146.913 239.271 146.913C239.548 146.913 239.769 146.96 239.937 147.055C240.111 147.142 240.199 147.309 240.199 147.556V147.633C240.199 147.873 240.111 148.036 239.937 148.124C239.762 148.211 239.54 148.255 239.271 148.255ZM244.185 156V149.455H245.276V150.545H245.603C245.763 150.175 246.014 149.884 246.356 149.673C246.697 149.455 247.086 149.345 247.523 149.345C248.177 149.345 248.694 149.545 249.072 149.945C249.45 150.345 249.639 150.891 249.639 151.582V156H248.548V151.8C248.548 151.342 248.436 150.993 248.21 150.753C247.992 150.505 247.676 150.382 247.261 150.382C246.934 150.382 246.599 150.48 246.257 150.676C245.923 150.873 245.596 151.156 245.276 151.527V156H244.185ZM262.771 156H261.626L258.811 147.273H259.99L262.215 154.615L264.44 147.273H265.586L262.771 156ZM269.976 156.065C269.321 156.065 268.71 155.96 268.143 155.749C267.576 155.538 267.067 155.153 266.616 154.593L267.401 153.851C267.707 154.265 268.07 154.575 268.492 154.778C268.921 154.982 269.427 155.084 270.008 155.084C270.547 155.084 270.979 154.96 271.307 154.713C271.641 154.458 271.808 154.116 271.808 153.687C271.808 153.273 271.667 152.956 271.383 152.738C271.099 152.513 270.539 152.276 269.703 152.029C268.641 151.724 267.917 151.393 267.532 151.036C267.147 150.68 266.954 150.175 266.954 149.52C266.954 148.764 267.205 148.193 267.707 147.807C268.216 147.422 268.83 147.229 269.55 147.229C270.161 147.229 270.739 147.338 271.285 147.556C271.837 147.767 272.325 148.145 272.747 148.691L271.994 149.444C271.703 149.029 271.339 148.72 270.903 148.516C270.467 148.313 270.023 148.211 269.572 148.211C269.114 148.211 268.747 148.305 268.47 148.495C268.194 148.676 268.056 148.985 268.056 149.422C268.056 149.807 268.205 150.113 268.503 150.338C268.808 150.556 269.405 150.793 270.292 151.047C271.296 151.338 271.979 151.658 272.343 152.007C272.714 152.349 272.899 152.847 272.899 153.502C272.899 154.338 272.601 154.975 272.005 155.411C271.408 155.847 270.732 156.065 269.976 156.065ZM285.224 156.109C284.206 156.109 283.442 155.851 282.933 155.335C282.424 154.811 282.17 154.025 282.17 152.978V150.338C282.17 149.276 282.424 148.484 282.933 147.96C283.442 147.429 284.21 147.164 285.235 147.164C286.239 147.164 286.981 147.4 287.461 147.873C287.948 148.338 288.221 149.087 288.279 150.12L287.188 150.229C287.115 149.444 286.937 148.902 286.653 148.604C286.377 148.298 285.901 148.145 285.224 148.145C284.504 148.145 283.995 148.313 283.697 148.647C283.406 148.975 283.261 149.538 283.261 150.338V152.978C283.261 153.764 283.406 154.32 283.697 154.647C283.995 154.967 284.504 155.127 285.224 155.127C285.901 155.127 286.377 154.982 286.653 154.691C286.937 154.393 287.115 153.858 287.188 153.087L288.279 153.196C288.221 154.215 287.948 154.956 287.461 155.422C286.981 155.88 286.235 156.109 285.224 156.109ZM292.756 156.109C291.876 156.109 291.199 155.898 290.727 155.476C290.261 155.047 290.028 154.433 290.028 153.633V151.865C290.028 151.058 290.265 150.436 290.738 150C291.21 149.564 291.887 149.345 292.767 149.345C293.639 149.345 294.308 149.564 294.774 150C295.247 150.436 295.483 151.058 295.483 151.865V153.633C295.483 154.433 295.247 155.047 294.774 155.476C294.308 155.898 293.636 156.109 292.756 156.109ZM292.756 155.127C293.308 155.127 293.719 155.004 293.988 154.756C294.258 154.509 294.392 154.135 294.392 153.633V151.865C294.392 151.342 294.258 150.956 293.988 150.709C293.719 150.455 293.308 150.327 292.756 150.327C292.203 150.327 291.792 150.455 291.523 150.709C291.254 150.956 291.119 151.342 291.119 151.865V153.633C291.119 154.135 291.254 154.509 291.523 154.756C291.792 155.004 292.203 155.127 292.756 155.127ZM300.069 155.073C300.425 155.073 300.764 154.978 301.084 154.789C301.404 154.6 301.72 154.313 302.033 153.927V150.72C301.807 150.582 301.567 150.48 301.313 150.415C301.058 150.342 300.785 150.305 300.494 150.305C299.905 150.305 299.469 150.436 299.185 150.698C298.902 150.953 298.76 151.342 298.76 151.865V153.622C298.76 154.109 298.869 154.473 299.087 154.713C299.305 154.953 299.633 155.073 300.069 155.073ZM301.705 154.909C301.516 155.287 301.254 155.582 300.92 155.793C300.585 156.004 300.211 156.109 299.796 156.109C299.12 156.109 298.596 155.891 298.225 155.455C297.854 155.018 297.669 154.407 297.669 153.622V151.844C297.669 151.051 297.916 150.436 298.411 150C298.905 149.564 299.596 149.345 300.484 149.345C300.738 149.345 301 149.375 301.269 149.433C301.538 149.484 301.793 149.556 302.033 149.651V147.273H303.124V156H302.033V154.909H301.705ZM310.786 154.56C310.59 155.105 310.291 155.502 309.891 155.749C309.491 155.989 308.877 156.109 308.048 156.109C307.182 156.109 306.51 155.902 306.03 155.487C305.55 155.065 305.31 154.444 305.31 153.622V151.865C305.31 151.022 305.561 150.393 306.062 149.978C306.571 149.556 307.233 149.345 308.048 149.345C308.87 149.345 309.528 149.56 310.022 149.989C310.517 150.411 310.764 151.062 310.764 151.942V152.945H306.401V153.633C306.401 154.178 306.535 154.575 306.804 154.822C307.081 155.062 307.491 155.182 308.037 155.182C308.561 155.182 308.942 155.109 309.182 154.964C309.422 154.818 309.604 154.582 309.728 154.255L310.786 154.56ZM306.401 151.865V152.127H309.673V151.964C309.673 151.375 309.535 150.945 309.259 150.676C308.99 150.407 308.582 150.273 308.037 150.273C307.491 150.273 307.081 150.404 306.804 150.665C306.535 150.92 306.401 151.32 306.401 151.865ZM315.241 152.727L314.914 147.273H316.441L316.114 152.727H315.241ZM315.688 156.109C315.325 156.109 315.056 156.036 314.881 155.891C314.714 155.738 314.63 155.505 314.63 155.193V154.887C314.63 154.567 314.714 154.331 314.881 154.178C315.056 154.025 315.325 153.949 315.688 153.949C316.052 153.949 316.318 154.025 316.485 154.178C316.652 154.331 316.736 154.567 316.736 154.887V155.193C316.736 155.505 316.648 155.738 316.474 155.891C316.307 156.036 316.045 156.109 315.688 156.109Z" fill="var(--vscode-foreground, #CCCCCC)" /> + <path d="M51.7166 128.818V119.727H54.8984V120.545H52.6257V128.109H54.8984V128.818H51.7166ZM57.4474 127V126.182H59.4474V120.818H59.1838C58.9777 121.182 58.7111 121.439 58.3838 121.591C58.0626 121.742 57.6899 121.782 57.2656 121.709L57.1747 120.818C57.6414 120.873 58.0687 120.809 58.4565 120.627C58.8444 120.439 59.1838 120.139 59.4747 119.727H60.3565V126.182H62.1747V127H57.4474ZM63.9964 128.818V128.109H66.2692V120.545H63.9964V119.727H67.1783V128.818H63.9964Z" fill="var(--vscode-foreground, #A0A0A0)" /> + </g> + <defs> + <filter id="filter0_d_30614476" x="0" y="29" width="566" height="158" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> + <feFlood flood-opacity="0" result="BackgroundImageFix" /> + <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> + <feOffset dy="2" /> + <feGaussianBlur stdDeviation="6" /> + <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0" /> + <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_30614476" /> + <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_30614476" result="shape" /> + </filter> + <clipPath id="clip0_30614476"> + <rect width="136.145" height="39.2998" fill="var(--vscode-list-activeSelectionIconForeground, #FFFFFF)" transform="translate(32)" /> + </clipPath> + </defs> +</svg> \ 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 + +<img src="create-environment.svg" alt="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 (<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>`</kbd>) 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 (<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>`</kbd>): + +``` +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 (<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>`</kbd>) 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 @@ +<svg width="526" height="353" viewBox="0 0 526 353" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g filter="url(#filter0_d_30214690)"> + <rect width="502" height="39" transform="translate(12 10)" fill="var(--vscode-editorGroupHeader-tabsBackground, #252526)" /> + <g clip-path="url(#clip0_30214690)"> + <rect width="114.931" height="39.2998" transform="translate(12 10)" fill="var(--vscode-tab-activeBackground, #1E1E1E)" /> + <path d="M35.2341 29.3299H33.9741C33.5341 29.3299 33.1808 29.4632 32.9141 29.7299C32.6608 29.9832 32.5341 30.3299 32.5341 30.7699V31.8899C32.5341 32.0232 32.4741 32.0899 32.3541 32.0899H31.7941C31.2075 32.0899 30.7875 31.8366 30.5341 31.3299C30.3341 30.9166 30.2341 30.5432 30.2341 30.2099C30.1541 29.3832 30.2141 28.6966 30.4141 28.1499C30.6275 27.5099 31.0208 27.1366 31.5941 27.0299H35.2141C35.3475 27.0299 35.4141 27.0099 35.4141 26.9699V26.6499L35.3341 26.6099C35.2808 26.5966 35.2475 26.5899 35.2341 26.5899H33.0941C33.0008 26.5899 32.9341 26.5699 32.8941 26.5299C32.8675 26.4899 32.8541 26.4232 32.8541 26.3299V25.5299C32.8541 25.0632 33.0408 24.7699 33.4141 24.6499C33.8275 24.4766 34.1408 24.3699 34.3541 24.3299C35.1541 24.1966 35.9008 24.2166 36.5941 24.3899C36.9408 24.4699 37.2341 24.5966 37.4741 24.7699C37.6208 24.9166 37.7208 25.0499 37.7741 25.1699C37.8541 25.3166 37.8808 25.4766 37.8541 25.6499V27.8899C37.8541 28.3299 37.7341 28.6699 37.4941 28.9099C37.2541 29.1499 36.9141 29.2699 36.4741 29.2699C36.1808 29.3099 35.7675 29.3299 35.2341 29.3299ZM33.4741 25.5899C33.4741 25.7232 33.5208 25.8432 33.6141 25.9499C33.7075 26.0432 33.8208 26.0899 33.9541 26.0899C34.0875 26.0899 34.2075 26.0366 34.3141 25.9299C34.4208 25.8232 34.4741 25.7099 34.4741 25.5899C34.4741 25.4699 34.4208 25.3632 34.3141 25.2699C34.2208 25.1766 34.1075 25.1166 33.9741 25.0899C33.8275 25.0899 33.7075 25.1432 33.6141 25.2499C33.5208 25.3432 33.4741 25.4566 33.4741 25.5899ZM35.7341 29.9699H36.9741C37.4141 29.9699 37.7608 29.8432 38.0141 29.5899C38.2808 29.3232 38.4141 28.9699 38.4141 28.5299V27.3899C38.4141 27.2699 38.4741 27.2099 38.5941 27.2099H39.1541C39.7408 27.2099 40.1608 27.4632 40.4141 27.9699C40.6275 28.3832 40.7341 28.7566 40.7341 29.0899C40.8008 29.9166 40.7341 30.6032 40.5341 31.1499C40.3208 31.7899 39.9275 32.1632 39.3541 32.2699H35.7341C35.6008 32.2699 35.5341 32.2899 35.5341 32.3299V32.6499L35.6141 32.6899C35.6675 32.7032 35.7075 32.7099 35.7341 32.7099H37.8541C37.9475 32.7099 38.0075 32.7299 38.0341 32.7699C38.0741 32.8099 38.0941 32.8766 38.0941 32.9699V33.7699C38.0941 34.2366 37.9075 34.5299 37.5341 34.6499C37.1208 34.8099 36.8075 34.9166 36.5941 34.9699C35.7941 35.1032 35.0475 35.0766 34.3541 34.8899C34.0075 34.8232 33.7141 34.7032 33.4741 34.5299C33.3275 34.3832 33.2275 34.2499 33.1741 34.1299C33.0941 33.9832 33.0675 33.8232 33.0941 33.6499V31.3899C33.0941 30.9632 33.2141 30.6299 33.4541 30.3899C33.6941 30.1499 34.0341 30.0299 34.4741 30.0299C34.7675 29.9899 35.1875 29.9699 35.7341 29.9699ZM37.4741 33.7099C37.4741 33.5766 37.4275 33.4632 37.3341 33.3699C37.2408 33.2632 37.1275 33.2099 36.9941 33.2099C36.8608 33.2099 36.7408 33.2632 36.6341 33.3699C36.5275 33.4766 36.4741 33.5899 36.4741 33.7099C36.4741 33.8299 36.5208 33.9366 36.6141 34.0299C36.7208 34.1232 36.8408 34.1832 36.9741 34.2099C37.1208 34.2099 37.2408 34.1632 37.3341 34.0699C37.4275 33.9632 37.4741 33.8432 37.4741 33.7099Z" fill="#519ABA" /> + <path d="M52.8667 34.7642C53.7744 34.7642 54.5298 34.3706 54.9424 33.6724H55.0503V34.6499H56.3643V29.9336C56.3643 28.4863 55.3867 27.623 53.6538 27.623C52.0859 27.623 50.9688 28.3784 50.8291 29.54H52.1494C52.3018 29.0386 52.8286 28.7529 53.5903 28.7529C54.5234 28.7529 55.0059 29.1782 55.0059 29.9336V30.5366L53.1333 30.6509C51.4893 30.7524 50.5625 31.4697 50.5625 32.7075C50.5625 33.9644 51.5337 34.7642 52.8667 34.7642ZM53.2158 33.666C52.4731 33.666 51.9336 33.2915 51.9336 32.6504C51.9336 32.022 52.3652 31.6855 53.3174 31.6221L55.0059 31.5078V32.1045C55.0059 32.9932 54.2441 33.666 53.2158 33.666ZM61.8931 27.6357C60.9536 27.6357 60.1411 28.1118 59.7222 28.8989H59.6206V27.7563H58.3066V36.9478H59.6714V33.6089H59.7793C60.1411 34.3389 60.9219 34.7642 61.9058 34.7642C63.6514 34.7642 64.7622 33.3804 64.7622 31.2031C64.7622 29.0132 63.6514 27.6357 61.8931 27.6357ZM61.5059 33.5962C60.3633 33.5962 59.646 32.6758 59.646 31.2031C59.646 29.7241 60.3633 28.8037 61.5122 28.8037C62.6675 28.8037 63.3594 29.7051 63.3594 31.2031C63.3594 32.7012 62.6675 33.5962 61.5059 33.5962ZM69.9927 27.6357C69.0532 27.6357 68.2407 28.1118 67.8218 28.8989H67.7202V27.7563H66.4062V36.9478H67.771V33.6089H67.8789C68.2407 34.3389 69.0215 34.7642 70.0054 34.7642C71.751 34.7642 72.8618 33.3804 72.8618 31.2031C72.8618 29.0132 71.751 27.6357 69.9927 27.6357ZM69.6055 33.5962C68.4629 33.5962 67.7456 32.6758 67.7456 31.2031C67.7456 29.7241 68.4629 28.8037 69.6118 28.8037C70.7671 28.8037 71.459 29.7051 71.459 31.2031C71.459 32.7012 70.7671 33.5962 69.6055 33.5962ZM75.4136 34.7451C75.9531 34.7451 76.3467 34.3452 76.3467 33.8311C76.3467 33.3169 75.9531 32.917 75.4136 32.917C74.8804 32.917 74.4805 33.3169 74.4805 33.8311C74.4805 34.3452 74.8804 34.7451 75.4136 34.7451ZM82.0405 27.6357C81.1011 27.6357 80.2886 28.1118 79.8696 28.8989H79.7681V27.7563H78.4541V36.9478H79.8188V33.6089H79.9268C80.2886 34.3389 81.0693 34.7642 82.0532 34.7642C83.7988 34.7642 84.9097 33.3804 84.9097 31.2031C84.9097 29.0132 83.7988 27.6357 82.0405 27.6357ZM81.6533 33.5962C80.5107 33.5962 79.7935 32.6758 79.7935 31.2031C79.7935 29.7241 80.5107 28.8037 81.6597 28.8037C82.8149 28.8037 83.5068 29.7051 83.5068 31.2031C83.5068 32.7012 82.8149 33.5962 81.6533 33.5962ZM86.9727 37.1509C88.3818 37.1509 89.0356 36.624 89.626 34.9673L92.2031 27.7563H90.7559L89.0293 33.2788H88.9214L87.1885 27.7563H85.7031L88.2041 34.6689L88.1025 35.0244C87.8677 35.7544 87.4995 36.0273 86.8521 36.0273C86.7251 36.0273 86.5156 36.021 86.4077 36.002V37.1191C86.5347 37.1382 86.8584 37.1509 86.9727 37.1509Z" fill="var(--vscode-tab-activeForeground, #FFFFFF)" /> + <path d="M105.457 30.3539L109.105 34.0019L109.809 33.2979L106.161 29.6499L109.809 26.0019L109.105 25.2979L105.457 28.9459L101.809 25.2979L101.105 26.0019L104.753 29.6499L101.105 33.2979L101.809 34.0019L105.457 30.3539Z" fill="var(--vscode-foreground, #CCCCCC)" /> + </g> + <g clip-path="url(#clip1_30214690)"> + <g opacity="0.5" clip-path="url(#clip2_30214690)"> + <rect width="143.931" height="39.2998" transform="translate(264 10)" fill="var(--vscode-tab-inactiveBackground, #1E1E1E)" /> + <path d="M285.852 30.4099H294.092V31.4899H285.852V30.4099ZM285.852 27.7899H290.452V28.8899H285.852V27.7899ZM285.852 25.1899H294.092V26.2899H285.852V25.1899ZM285.852 33.0099H291.972V34.1099H285.852V33.0099Z" fill="#D4D7D6" /> + <path d="M309.537 34.6499V25.4902H308.115V34.6499H309.537ZM311.676 34.6499H313.041V30.6064C313.041 29.4956 313.682 28.8101 314.691 28.8101C315.7 28.8101 316.183 29.3687 316.183 30.5112V34.6499H317.547V30.1875C317.547 28.5435 316.697 27.623 315.154 27.623C314.113 27.623 313.428 28.0864 313.091 28.8481H312.99V27.7563H311.676V34.6499ZM319.953 26.0171V27.7754H318.855V28.8672H319.953V32.8345C319.953 34.1611 320.556 34.6943 322.073 34.6943C322.34 34.6943 322.594 34.6626 322.816 34.6245V33.5391C322.625 33.5581 322.505 33.5708 322.295 33.5708C321.616 33.5708 321.318 33.2471 321.318 32.5044V28.8672H322.816V27.7754H321.318V26.0171H319.953ZM328.878 32.8027C328.624 33.3613 328.059 33.666 327.234 33.666C326.142 33.666 325.438 32.8789 325.393 31.6284V31.5649H330.274V31.0952C330.274 28.937 329.113 27.623 327.189 27.623C325.241 27.623 324.003 29.0259 324.003 31.2158C324.003 33.4185 325.215 34.7832 327.196 34.7832C328.776 34.7832 329.894 34.0215 330.192 32.8027H328.878ZM327.183 28.7339C328.192 28.7339 328.853 29.4639 328.884 30.5811H325.393C325.469 29.4702 326.174 28.7339 327.183 28.7339ZM331.912 34.6499H333.277V30.5303C333.277 29.5591 334.007 28.8926 335.042 28.8926C335.283 28.8926 335.689 28.937 335.803 28.9688V27.6865C335.657 27.6548 335.397 27.6357 335.194 27.6357C334.292 27.6357 333.524 28.1245 333.328 28.8037H333.226V27.7563H331.912V34.6499ZM338.977 34.7642C339.885 34.7642 340.64 34.3706 341.053 33.6724H341.161V34.6499H342.475V29.9336C342.475 28.4863 341.497 27.623 339.764 27.623C338.196 27.623 337.079 28.3784 336.939 29.54H338.26C338.412 29.0386 338.939 28.7529 339.701 28.7529C340.634 28.7529 341.116 29.1782 341.116 29.9336V30.5366L339.244 30.6509C337.6 30.7524 336.673 31.4697 336.673 32.7075C336.673 33.9644 337.644 34.7642 338.977 34.7642ZM339.326 33.666C338.583 33.666 338.044 33.2915 338.044 32.6504C338.044 32.022 338.476 31.6855 339.428 31.6221L341.116 31.5078V32.1045C341.116 32.9932 340.354 33.666 339.326 33.666ZM350.27 29.9717C350.092 28.6323 349.044 27.623 347.312 27.623C345.299 27.623 344.074 28.9878 344.074 31.1777C344.074 33.4121 345.306 34.7832 347.318 34.7832C349.025 34.7832 350.092 33.8247 350.27 32.4663H348.943C348.765 33.2153 348.175 33.6216 347.312 33.6216C346.175 33.6216 345.471 32.7012 345.471 31.1777C345.471 29.686 346.169 28.7847 347.312 28.7847C348.226 28.7847 348.784 29.2988 348.943 29.9717H350.27ZM352.415 26.0171V27.7754H351.317V28.8672H352.415V32.8345C352.415 34.1611 353.018 34.6943 354.535 34.6943C354.802 34.6943 355.056 34.6626 355.278 34.6245V33.5391C355.087 33.5581 354.967 33.5708 354.757 33.5708C354.078 33.5708 353.78 33.2471 353.78 32.5044V28.8672H355.278V27.7754H353.78V26.0171H352.415ZM357.588 26.5249C358.064 26.5249 358.452 26.1377 358.452 25.668C358.452 25.1919 358.064 24.8047 357.588 24.8047C357.112 24.8047 356.725 25.1919 356.725 25.668C356.725 26.1377 357.112 26.5249 357.588 26.5249ZM356.909 34.6499H358.268V27.7563H356.909V34.6499ZM366.126 27.7563H364.679L362.965 33.2534H362.857L361.137 27.7563H359.677L362.171 34.6499H363.638L366.126 27.7563ZM371.775 32.8027C371.521 33.3613 370.957 33.666 370.131 33.666C369.04 33.666 368.335 32.8789 368.291 31.6284V31.5649H373.172V31.0952C373.172 28.937 372.01 27.623 370.087 27.623C368.138 27.623 366.9 29.0259 366.9 31.2158C366.9 33.4185 368.113 34.7832 370.093 34.7832C371.674 34.7832 372.791 34.0215 373.089 32.8027H371.775ZM370.081 28.7339C371.09 28.7339 371.75 29.4639 371.782 30.5811H368.291C368.367 29.4702 369.071 28.7339 370.081 28.7339Z" fill="var(--vscode-tab-inactiveForeground, #FFFFFF)" /> + <path d="M386.457 30.3539L390.105 34.0019L390.809 33.2979L387.161 29.6499L390.809 26.0019L390.105 25.2979L386.457 28.9459L382.809 25.2979L382.105 26.0019L385.753 29.6499L382.105 33.2979L382.809 34.0019L386.457 30.3539Z" fill="var(--vscode-foreground, #CCCCCC)" /> + </g> + </g> + <path d="M12 49H514V336C514 337.105 513.105 338 512 338H14C12.8954 338 12 337.105 12 336V49Z" fill="var(--vscode-editor-background, #1E1E1E)" /> + <rect x="36" y="145" width="32" height="4" rx="2" fill="var(--vscode-editorOverviewRuler-commonContentForeground, #606060)" /> + <rect x="72" y="145" width="52" height="4" rx="2" fill="var(--vscode-editorOverviewRuler-commonContentForeground, #606060)" /> + <rect x="128" y="145" width="36" height="4" rx="2" fill="var(--vscode-editorOverviewRuler-commonContentForeground, #606060)" /> + <path d="M40.5273 82.8364L40.6473 80.4364H39.12L39 82.8364H40.5273ZM40.3636 86L40.4727 83.8182H38.9455L38.8364 86H37.8545L37.9636 83.8182H36.5455V82.8364H38.0182L38.1382 80.4364H36.5455V79.4545H38.1818L38.2909 77.2727H39.2727L39.1636 79.4545H40.6909L40.8 77.2727H41.7818L41.6727 79.4545H43.0909V80.4364H41.6291L41.5091 82.8364H43.0909V83.8182H41.4545L41.3455 86H40.3636ZM53.4085 81.1127C52.9067 81.1127 52.4994 80.9745 52.1867 80.6982C51.874 80.4218 51.7176 79.9964 51.7176 79.4218V78.7673C51.7176 78.1855 51.874 77.7564 52.1867 77.48C52.4994 77.1964 52.9067 77.0545 53.4085 77.0545C53.9103 77.0545 54.314 77.1964 54.6194 77.48C54.9322 77.7564 55.0885 78.1855 55.0885 78.7673V79.4218C55.0885 79.9964 54.9322 80.4218 54.6194 80.6982C54.314 80.9745 53.9103 81.1127 53.4085 81.1127ZM53.4085 80.2291C53.6267 80.2291 53.7976 80.1673 53.9212 80.0436C54.0449 79.92 54.1067 79.72 54.1067 79.4436V78.7455C54.1067 78.4691 54.0449 78.2655 53.9212 78.1345C53.7976 78.0036 53.6267 77.9382 53.4085 77.9382C53.1903 77.9382 53.0158 78.0036 52.8849 78.1345C52.7613 78.2655 52.6994 78.4691 52.6994 78.7455V79.4436C52.6994 79.72 52.7613 79.92 52.8849 80.0436C53.0158 80.1673 53.1903 80.2291 53.4085 80.2291ZM51.7722 84.1673L57.8813 78.3964L58.4813 79.0291L52.3613 84.8L51.7722 84.1673ZM56.8013 86.24C56.2994 86.24 55.8922 86.1018 55.5794 85.8255C55.2667 85.5491 55.1103 85.1236 55.1103 84.5491V83.8945C55.1103 83.3127 55.2667 82.8836 55.5794 82.6073C55.8922 82.3236 56.2994 82.1818 56.8013 82.1818C57.3031 82.1818 57.7067 82.3236 58.0122 82.6073C58.3249 82.8836 58.4813 83.3127 58.4813 83.8945V84.5491C58.4813 85.1236 58.3249 85.5491 58.0122 85.8255C57.7067 86.1018 57.3031 86.24 56.8013 86.24ZM56.8013 85.3564C57.0194 85.3564 57.1903 85.2945 57.314 85.1709C57.4376 85.0473 57.4994 84.8473 57.4994 84.5709V83.8727C57.4994 83.5964 57.4376 83.3927 57.314 83.2618C57.1903 83.1309 57.0194 83.0655 56.8013 83.0655C56.5831 83.0655 56.4085 83.1309 56.2776 83.2618C56.154 83.3927 56.0922 83.5964 56.0922 83.8727V84.5709C56.0922 84.8473 56.154 85.0473 56.2776 85.1709C56.4085 85.2945 56.5831 85.3564 56.8013 85.3564ZM61.0491 81.1127C60.5473 81.1127 60.1401 80.9745 59.8273 80.6982C59.5146 80.4218 59.3582 79.9964 59.3582 79.4218V78.7673C59.3582 78.1855 59.5146 77.7564 59.8273 77.48C60.1401 77.1964 60.5473 77.0545 61.0491 77.0545C61.551 77.0545 61.9546 77.1964 62.2601 77.48C62.5728 77.7564 62.7291 78.1855 62.7291 78.7673V79.4218C62.7291 79.9964 62.5728 80.4218 62.2601 80.6982C61.9546 80.9745 61.551 81.1127 61.0491 81.1127ZM61.0491 80.2291C61.2673 80.2291 61.4382 80.1673 61.5619 80.0436C61.6855 79.92 61.7473 79.72 61.7473 79.4436V78.7455C61.7473 78.4691 61.6855 78.2655 61.5619 78.1345C61.4382 78.0036 61.2673 77.9382 61.0491 77.9382C60.831 77.9382 60.6564 78.0036 60.5255 78.1345C60.4019 78.2655 60.3401 78.4691 60.3401 78.7455V79.4436C60.3401 79.72 60.4019 79.92 60.5255 80.0436C60.6564 80.1673 60.831 80.2291 61.0491 80.2291ZM59.4128 84.1673L65.5219 78.3964L66.1219 79.0291L60.0019 84.8L59.4128 84.1673ZM64.4419 86.24C63.9401 86.24 63.5328 86.1018 63.2201 85.8255C62.9073 85.5491 62.751 85.1236 62.751 84.5491V83.8945C62.751 83.3127 62.9073 82.8836 63.2201 82.6073C63.5328 82.3236 63.9401 82.1818 64.4419 82.1818C64.9437 82.1818 65.3473 82.3236 65.6528 82.6073C65.9655 82.8836 66.1219 83.3127 66.1219 83.8945V84.5491C66.1219 85.1236 65.9655 85.5491 65.6528 85.8255C65.3473 86.1018 64.9437 86.24 64.4419 86.24ZM64.4419 85.3564C64.6601 85.3564 64.831 85.2945 64.9546 85.1709C65.0782 85.0473 65.1401 84.8473 65.1401 84.5709V83.8727C65.1401 83.5964 65.0782 83.3927 64.9546 83.2618C64.831 83.1309 64.6601 83.0655 64.4419 83.0655C64.2237 83.0655 64.0491 83.1309 63.9182 83.2618C63.7946 83.3927 63.7328 83.5964 63.7328 83.8727V84.5709C63.7328 84.8473 63.7946 85.0473 63.9182 85.1709C64.0491 85.2945 64.2237 85.3564 64.4419 85.3564ZM40.5273 172.836L40.6473 170.436H39.12L39 172.836H40.5273ZM40.3636 176L40.4727 173.818H38.9455L38.8364 176H37.8545L37.9636 173.818H36.5455V172.836H38.0182L38.1382 170.436H36.5455V169.455H38.1818L38.2909 167.273H39.2727L39.1636 169.455H40.6909L40.8 167.273H41.7818L41.6727 169.455H43.0909V170.436H41.6291L41.5091 172.836H43.0909V173.818H41.4545L41.3455 176H40.3636ZM53.4085 171.113C52.9067 171.113 52.4994 170.975 52.1867 170.698C51.874 170.422 51.7176 169.996 51.7176 169.422V168.767C51.7176 168.185 51.874 167.756 52.1867 167.48C52.4994 167.196 52.9067 167.055 53.4085 167.055C53.9103 167.055 54.314 167.196 54.6194 167.48C54.9322 167.756 55.0885 168.185 55.0885 168.767V169.422C55.0885 169.996 54.9322 170.422 54.6194 170.698C54.314 170.975 53.9103 171.113 53.4085 171.113ZM53.4085 170.229C53.6267 170.229 53.7976 170.167 53.9212 170.044C54.0449 169.92 54.1067 169.72 54.1067 169.444V168.745C54.1067 168.469 54.0449 168.265 53.9212 168.135C53.7976 168.004 53.6267 167.938 53.4085 167.938C53.1903 167.938 53.0158 168.004 52.8849 168.135C52.7613 168.265 52.6994 168.469 52.6994 168.745V169.444C52.6994 169.72 52.7613 169.92 52.8849 170.044C53.0158 170.167 53.1903 170.229 53.4085 170.229ZM51.7722 174.167L57.8813 168.396L58.4813 169.029L52.3613 174.8L51.7722 174.167ZM56.8013 176.24C56.2994 176.24 55.8922 176.102 55.5794 175.825C55.2667 175.549 55.1103 175.124 55.1103 174.549V173.895C55.1103 173.313 55.2667 172.884 55.5794 172.607C55.8922 172.324 56.2994 172.182 56.8013 172.182C57.3031 172.182 57.7067 172.324 58.0122 172.607C58.3249 172.884 58.4813 173.313 58.4813 173.895V174.549C58.4813 175.124 58.3249 175.549 58.0122 175.825C57.7067 176.102 57.3031 176.24 56.8013 176.24ZM56.8013 175.356C57.0194 175.356 57.1903 175.295 57.314 175.171C57.4376 175.047 57.4994 174.847 57.4994 174.571V173.873C57.4994 173.596 57.4376 173.393 57.314 173.262C57.1903 173.131 57.0194 173.065 56.8013 173.065C56.5831 173.065 56.4085 173.131 56.2776 173.262C56.154 173.393 56.0922 173.596 56.0922 173.873V174.571C56.0922 174.847 56.154 175.047 56.2776 175.171C56.4085 175.295 56.5831 175.356 56.8013 175.356ZM61.0491 171.113C60.5473 171.113 60.1401 170.975 59.8273 170.698C59.5146 170.422 59.3582 169.996 59.3582 169.422V168.767C59.3582 168.185 59.5146 167.756 59.8273 167.48C60.1401 167.196 60.5473 167.055 61.0491 167.055C61.551 167.055 61.9546 167.196 62.2601 167.48C62.5728 167.756 62.7291 168.185 62.7291 168.767V169.422C62.7291 169.996 62.5728 170.422 62.2601 170.698C61.9546 170.975 61.551 171.113 61.0491 171.113ZM61.0491 170.229C61.2673 170.229 61.4382 170.167 61.5619 170.044C61.6855 169.92 61.7473 169.72 61.7473 169.444V168.745C61.7473 168.469 61.6855 168.265 61.5619 168.135C61.4382 168.004 61.2673 167.938 61.0491 167.938C60.831 167.938 60.6564 168.004 60.5255 168.135C60.4019 168.265 60.3401 168.469 60.3401 168.745V169.444C60.3401 169.72 60.4019 169.92 60.5255 170.044C60.6564 170.167 60.831 170.229 61.0491 170.229ZM59.4128 174.167L65.5219 168.396L66.1219 169.029L60.0019 174.8L59.4128 174.167ZM64.4419 176.24C63.9401 176.24 63.5328 176.102 63.2201 175.825C62.9073 175.549 62.751 175.124 62.751 174.549V173.895C62.751 173.313 62.9073 172.884 63.2201 172.607C63.5328 172.324 63.9401 172.182 64.4419 172.182C64.9437 172.182 65.3473 172.324 65.6528 172.607C65.9655 172.884 66.1219 173.313 66.1219 173.895V174.549C66.1219 175.124 65.9655 175.549 65.6528 175.825C65.3473 176.102 64.9437 176.24 64.4419 176.24ZM64.4419 175.356C64.6601 175.356 64.831 175.295 64.9546 175.171C65.0782 175.047 65.1401 174.847 65.1401 174.571V173.873C65.1401 173.596 65.0782 173.393 64.9546 173.262C64.831 173.131 64.6601 173.065 64.4419 173.065C64.2237 173.065 64.0491 173.131 63.9182 173.262C63.7946 173.393 63.7328 173.596 63.7328 173.873V174.571C63.7328 174.847 63.7946 175.047 63.9182 175.171C64.0491 175.295 64.2237 175.356 64.4419 175.356Z" fill="var(--vscode-editorIndentGuide-activeBackground, #6B737C)" /> + <path d="M37.6582 99.3091V104H36.6545V97.4545H37.6582V98.3273H37.8764C38.0145 98.0218 38.1964 97.7818 38.4218 97.6073C38.6473 97.4327 38.8836 97.3455 39.1309 97.3455C39.4145 97.3455 39.6509 97.4327 39.84 97.6073C40.0364 97.7745 40.1709 98.0145 40.2436 98.3273H40.4836C40.6218 98.0218 40.8073 97.7818 41.04 97.6073C41.28 97.4327 41.5345 97.3455 41.8036 97.3455C42.1673 97.3455 42.4545 97.4945 42.6655 97.7927C42.8764 98.0836 42.9818 98.48 42.9818 98.9818V104H41.9782V99.1564C41.9782 98.8291 41.9345 98.5818 41.8473 98.4145C41.76 98.2473 41.6327 98.1636 41.4655 98.1636C41.3273 98.1636 41.1527 98.2655 40.9418 98.4691C40.7382 98.6727 40.5309 98.9527 40.32 99.3091V104H39.3164V99.1564C39.3164 98.8291 39.2691 98.5818 39.1745 98.4145C39.0873 98.2473 38.96 98.1636 38.7927 98.1636C38.6473 98.1636 38.4764 98.2618 38.28 98.4582C38.0909 98.6545 37.8836 98.9382 37.6582 99.3091ZM47.5461 104.109C46.9934 104.109 46.4588 104.029 45.9424 103.869C45.4261 103.702 44.9497 103.447 44.5134 103.105L45.0043 102.244C45.3606 102.535 45.757 102.756 46.1934 102.909C46.637 103.055 47.0879 103.127 47.5461 103.127C47.9243 103.127 48.2843 103.069 48.6261 102.953C48.9752 102.836 49.1497 102.589 49.1497 102.211C49.1497 101.898 49.0079 101.676 48.7243 101.545C48.4406 101.415 47.9206 101.302 47.1643 101.207C46.3643 101.113 45.757 100.927 45.3424 100.651C44.9352 100.367 44.7315 99.9236 44.7315 99.32C44.7315 98.6655 44.9788 98.1745 45.4734 97.8473C45.9679 97.5127 46.5424 97.3455 47.197 97.3455C47.7134 97.3455 48.2079 97.4327 48.6806 97.6073C49.1606 97.7818 49.6043 98.0291 50.0115 98.3491L49.5206 99.2109C49.1861 98.9418 48.8224 98.7273 48.4297 98.5673C48.037 98.4073 47.6261 98.3273 47.197 98.3273C46.8479 98.3273 46.5279 98.3927 46.237 98.5236C45.9534 98.6545 45.8115 98.8945 45.8115 99.2436C45.8115 99.5418 45.9424 99.76 46.2043 99.8982C46.4734 100.029 46.9861 100.142 47.7424 100.236C48.5715 100.331 49.1934 100.535 49.6079 100.847C50.0224 101.153 50.2297 101.582 50.2297 102.135C50.2297 102.818 49.9534 103.32 49.4006 103.64C48.8479 103.953 48.2297 104.109 47.5461 104.109ZM58.3722 104.524C58.3722 105.113 58.154 105.553 57.7176 105.844C57.2885 106.142 56.7613 106.291 56.1358 106.291H54.0849C53.4303 106.291 52.8885 106.16 52.4594 105.898C52.0376 105.636 51.8267 105.233 51.8267 104.687C51.8267 104.338 51.9176 104.062 52.0994 103.858C52.2885 103.647 52.5613 103.527 52.9176 103.498V103.269C52.6631 103.225 52.4522 103.12 52.2849 102.953C52.1249 102.785 52.0449 102.527 52.0449 102.178C52.0449 101.865 52.1431 101.585 52.3394 101.338C52.5358 101.084 52.8667 100.938 53.3322 100.902V100.695C52.9758 100.578 52.7358 100.404 52.6122 100.171C52.4885 99.9309 52.4267 99.7018 52.4267 99.4836V99.1891C52.4267 98.6873 52.6413 98.2727 53.0703 97.9455C53.4994 97.6182 54.1031 97.4545 54.8813 97.4545H58.0994V98.4364L56.4085 98.1855V98.3818C56.6994 98.4618 56.9176 98.6145 57.0631 98.84C57.2158 99.0655 57.2922 99.2909 57.2922 99.5164V99.8C57.2922 100.302 57.0994 100.709 56.714 101.022C56.3285 101.327 55.7031 101.48 54.8376 101.48H53.5176C53.3212 101.48 53.1722 101.531 53.0703 101.633C52.9685 101.727 52.9176 101.891 52.9176 102.124C52.9176 102.349 52.9867 102.52 53.1249 102.636C53.2703 102.745 53.4849 102.8 53.7685 102.8H56.2449C56.8558 102.8 57.3613 102.935 57.7613 103.204C58.1685 103.473 58.3722 103.913 58.3722 104.524ZM54.8813 100.651C55.3249 100.651 55.6631 100.571 55.8958 100.411C56.1285 100.251 56.2449 100 56.2449 99.6582V99.3091C56.2449 98.9673 56.1285 98.72 55.8958 98.5673C55.6631 98.4073 55.3249 98.3273 54.8813 98.3273C54.4376 98.3273 54.0994 98.4073 53.8667 98.5673C53.634 98.72 53.5176 98.9673 53.5176 99.3091V99.6582C53.5176 100 53.634 100.251 53.8667 100.411C54.0994 100.571 54.4376 100.651 54.8813 100.651ZM54.0631 105.418H56.2776C56.6049 105.418 56.8703 105.356 57.074 105.233C57.2776 105.109 57.3794 104.895 57.3794 104.589C57.3794 104.298 57.2849 104.091 57.0958 103.967C56.914 103.844 56.6776 103.782 56.3867 103.782H52.9176V104.524C52.9176 104.822 53.0158 105.044 53.2122 105.189C53.4085 105.342 53.6922 105.418 54.0631 105.418ZM83.5019 189.309V194H82.4983V187.455H83.5019V188.327H83.7201C83.8583 188.022 84.0401 187.782 84.2656 187.607C84.491 187.433 84.7274 187.345 84.9747 187.345C85.2583 187.345 85.4947 187.433 85.6837 187.607C85.8801 187.775 86.0147 188.015 86.0874 188.327H86.3274C86.4656 188.022 86.651 187.782 86.8838 187.607C87.1238 187.433 87.3783 187.345 87.6474 187.345C88.011 187.345 88.2983 187.495 88.5092 187.793C88.7201 188.084 88.8256 188.48 88.8256 188.982V194H87.8219V189.156C87.8219 188.829 87.7783 188.582 87.691 188.415C87.6037 188.247 87.4765 188.164 87.3092 188.164C87.171 188.164 86.9965 188.265 86.7856 188.469C86.5819 188.673 86.3747 188.953 86.1638 189.309V194H85.1601V189.156C85.1601 188.829 85.1128 188.582 85.0183 188.415C84.931 188.247 84.8037 188.164 84.6365 188.164C84.491 188.164 84.3201 188.262 84.1237 188.458C83.9347 188.655 83.7274 188.938 83.5019 189.309ZM93.3898 194.109C92.8371 194.109 92.3026 194.029 91.7862 193.869C91.2698 193.702 90.7935 193.447 90.3571 193.105L90.848 192.244C91.2044 192.535 91.6007 192.756 92.0371 192.909C92.4807 193.055 92.9316 193.127 93.3898 193.127C93.768 193.127 94.128 193.069 94.4698 192.953C94.8189 192.836 94.9935 192.589 94.9935 192.211C94.9935 191.898 94.8516 191.676 94.568 191.545C94.2844 191.415 93.7644 191.302 93.008 191.207C92.208 191.113 91.6007 190.927 91.1862 190.651C90.7789 190.367 90.5753 189.924 90.5753 189.32C90.5753 188.665 90.8226 188.175 91.3171 187.847C91.8116 187.513 92.3862 187.345 93.0407 187.345C93.5571 187.345 94.0516 187.433 94.5244 187.607C95.0044 187.782 95.448 188.029 95.8553 188.349L95.3644 189.211C95.0298 188.942 94.6662 188.727 94.2735 188.567C93.8807 188.407 93.4698 188.327 93.0407 188.327C92.6916 188.327 92.3716 188.393 92.0807 188.524C91.7971 188.655 91.6553 188.895 91.6553 189.244C91.6553 189.542 91.7862 189.76 92.048 189.898C92.3171 190.029 92.8298 190.142 93.5862 190.236C94.4153 190.331 95.0371 190.535 95.4516 190.847C95.8662 191.153 96.0735 191.582 96.0735 192.135C96.0735 192.818 95.7971 193.32 95.2444 193.64C94.6916 193.953 94.0735 194.109 93.3898 194.109ZM104.216 194.524C104.216 195.113 103.998 195.553 103.561 195.844C103.132 196.142 102.605 196.291 101.98 196.291H99.9286C99.2741 196.291 98.7323 196.16 98.3032 195.898C97.8814 195.636 97.6705 195.233 97.6705 194.687C97.6705 194.338 97.7614 194.062 97.9432 193.858C98.1323 193.647 98.405 193.527 98.7614 193.498V193.269C98.5068 193.225 98.2959 193.12 98.1286 192.953C97.9686 192.785 97.8886 192.527 97.8886 192.178C97.8886 191.865 97.9868 191.585 98.1832 191.338C98.3795 191.084 98.7105 190.938 99.1759 190.902V190.695C98.8195 190.578 98.5795 190.404 98.4559 190.171C98.3323 189.931 98.2705 189.702 98.2705 189.484V189.189C98.2705 188.687 98.485 188.273 98.9141 187.945C99.3432 187.618 99.9468 187.455 100.725 187.455H103.943V188.436L102.252 188.185V188.382C102.543 188.462 102.761 188.615 102.907 188.84C103.06 189.065 103.136 189.291 103.136 189.516V189.8C103.136 190.302 102.943 190.709 102.558 191.022C102.172 191.327 101.547 191.48 100.681 191.48H99.3614C99.165 191.48 99.0159 191.531 98.9141 191.633C98.8123 191.727 98.7614 191.891 98.7614 192.124C98.7614 192.349 98.8305 192.52 98.9686 192.636C99.1141 192.745 99.3286 192.8 99.6123 192.8H102.089C102.7 192.8 103.205 192.935 103.605 193.204C104.012 193.473 104.216 193.913 104.216 194.524ZM100.725 190.651C101.169 190.651 101.507 190.571 101.74 190.411C101.972 190.251 102.089 190 102.089 189.658V189.309C102.089 188.967 101.972 188.72 101.74 188.567C101.507 188.407 101.169 188.327 100.725 188.327C100.281 188.327 99.9432 188.407 99.7105 188.567C99.4777 188.72 99.3614 188.967 99.3614 189.309V189.658C99.3614 190 99.4777 190.251 99.7105 190.411C99.9432 190.571 100.281 190.651 100.725 190.651ZM99.9068 195.418H102.121C102.449 195.418 102.714 195.356 102.918 195.233C103.121 195.109 103.223 194.895 103.223 194.589C103.223 194.298 103.129 194.091 102.94 193.967C102.758 193.844 102.521 193.782 102.23 193.782H98.7614V194.524C98.7614 194.822 98.8595 195.044 99.0559 195.189C99.2523 195.342 99.5359 195.418 99.9068 195.418Z" fill="var(--vscode-debugIcon-continueForeground, #9CDCFE)" /> + <path d="M67.3261 99.2V98.1091H73.4352V99.2H67.3261ZM67.3261 102.255V101.164H73.4352V102.255H67.3261ZM80.2031 196.291C79.0613 196.022 78.1377 195.4 77.4322 194.425C76.734 193.451 76.3849 192.218 76.3849 190.727C76.3849 189.236 76.734 188.004 77.4322 187.029C78.1377 186.055 79.0613 185.433 80.2031 185.164L80.3122 186.015C79.3377 186.284 78.6213 186.847 78.1631 187.705C77.7049 188.556 77.4759 189.564 77.4759 190.727C77.4759 191.891 77.7049 192.902 78.1631 193.76C78.6213 194.611 79.3377 195.171 80.3122 195.44L80.2031 196.291ZM106.402 196.291L106.293 195.44C107.267 195.171 107.984 194.607 108.442 193.749C108.9 192.884 109.129 191.876 109.129 190.727C109.129 189.578 108.9 188.575 108.442 187.716C107.984 186.851 107.267 186.284 106.293 186.015L106.402 185.164C107.544 185.433 108.464 186.058 109.162 187.04C109.867 188.015 110.22 189.244 110.22 190.727C110.22 192.211 109.867 193.44 109.162 194.415C108.464 195.396 107.544 196.022 106.402 196.291Z" fill="var(--vscode-editor-foreground, #D4D4D4)" /> + <path d="M86.7528 99.6364L86.4256 95.2727H88.171L87.8438 99.6364H86.7528ZM83.4801 99.6364L83.1528 95.2727H84.8983L84.571 99.6364H83.4801ZM90.5753 104V95.2727H91.6662V98.7636H94.9389V95.2727H96.0298V104H94.9389V99.7455H91.6662V104H90.5753ZM103.692 102.56C103.496 103.105 103.198 103.502 102.798 103.749C102.398 103.989 101.783 104.109 100.954 104.109C100.089 104.109 99.4159 103.902 98.9359 103.487C98.4559 103.065 98.2159 102.444 98.2159 101.622V99.8655C98.2159 99.0218 98.4668 98.3927 98.9686 97.9782C99.4777 97.5564 100.14 97.3455 100.954 97.3455C101.776 97.3455 102.434 97.56 102.929 97.9891C103.423 98.4109 103.67 99.0618 103.67 99.9418V100.945H99.3068V101.633C99.3068 102.178 99.4414 102.575 99.7105 102.822C99.9868 103.062 100.398 103.182 100.943 103.182C101.467 103.182 101.849 103.109 102.089 102.964C102.329 102.818 102.51 102.582 102.634 102.255L103.692 102.56ZM99.3068 99.8655V100.127H102.58V99.9636C102.58 99.3745 102.441 98.9455 102.165 98.6764C101.896 98.4073 101.489 98.2727 100.943 98.2727C100.398 98.2727 99.9868 98.4036 99.7105 98.6655C99.4414 98.92 99.3068 99.32 99.3068 99.8655ZM105.857 104V103.018H108.038V96.2545H105.857V95.2727H109.129V103.018H111.311V104H105.857ZM113.497 104V103.018H115.679V96.2545H113.497V95.2727H116.77V103.018H118.952V104H113.497ZM123.865 104.109C122.985 104.109 122.309 103.898 121.836 103.476C121.371 103.047 121.138 102.433 121.138 101.633V99.8655C121.138 99.0582 121.374 98.4364 121.847 98C122.32 97.5636 122.996 97.3455 123.876 97.3455C124.749 97.3455 125.418 97.5636 125.883 98C126.356 98.4364 126.592 99.0582 126.592 99.8655V101.633C126.592 102.433 126.356 103.047 125.883 103.476C125.418 103.898 124.745 104.109 123.865 104.109ZM123.865 103.127C124.418 103.127 124.829 103.004 125.098 102.756C125.367 102.509 125.501 102.135 125.501 101.633V99.8655C125.501 99.3418 125.367 98.9564 125.098 98.7091C124.829 98.4545 124.418 98.3273 123.865 98.3273C123.312 98.3273 122.901 98.4545 122.632 98.7091C122.363 98.9564 122.229 99.3418 122.229 99.8655V101.633C122.229 102.135 122.363 102.509 122.632 102.756C122.901 103.004 123.312 103.127 123.865 103.127ZM136.419 104V103.018H138.601V96.2545H136.419V95.2727H141.874V96.2545H139.692V103.018H141.874V104H136.419ZM144.06 104V97.4545H145.151V98.5455H145.478C145.638 98.1745 145.889 97.8836 146.231 97.6727C146.572 97.4545 146.961 97.3455 147.398 97.3455C148.052 97.3455 148.569 97.5455 148.947 97.9455C149.325 98.3455 149.514 98.8909 149.514 99.5818V104H148.423V99.8C148.423 99.3418 148.311 98.9927 148.085 98.7527C147.867 98.5055 147.551 98.3818 147.136 98.3818C146.809 98.3818 146.474 98.48 146.132 98.6764C145.798 98.8727 145.471 99.1564 145.151 99.5273V104H144.06ZM155.508 104.098C154.788 104.098 154.228 103.92 153.828 103.564C153.428 103.207 153.228 102.662 153.228 101.927V98.4364H151.373V97.4545H153.228V95.2727H154.318V97.4545H157.155V98.4364H154.318V101.829C154.318 102.251 154.424 102.564 154.635 102.767C154.846 102.971 155.158 103.073 155.573 103.073C155.849 103.073 156.108 103.055 156.348 103.018C156.588 102.975 156.817 102.92 157.035 102.855L157.253 103.847C157.006 103.92 156.74 103.978 156.457 104.022C156.18 104.073 155.864 104.098 155.508 104.098ZM164.817 102.56C164.621 103.105 164.323 103.502 163.923 103.749C163.523 103.989 162.908 104.109 162.079 104.109C161.214 104.109 160.541 103.902 160.061 103.487C159.581 103.065 159.341 102.444 159.341 101.622V99.8655C159.341 99.0218 159.592 98.3927 160.094 97.9782C160.603 97.5564 161.265 97.3455 162.079 97.3455C162.901 97.3455 163.559 97.56 164.054 97.9891C164.548 98.4109 164.795 99.0618 164.795 99.9418V100.945H160.432V101.633C160.432 102.178 160.566 102.575 160.835 102.822C161.112 103.062 161.523 103.182 162.068 103.182C162.592 103.182 162.974 103.109 163.214 102.964C163.454 102.818 163.635 102.582 163.759 102.255L164.817 102.56ZM160.432 99.8655V100.127H163.705V99.9636C163.705 99.3745 163.566 98.9455 163.29 98.6764C163.021 98.4073 162.614 98.2727 162.068 98.2727C161.523 98.2727 161.112 98.4036 160.835 98.6655C160.566 98.92 160.432 99.32 160.432 99.8655ZM167.527 104V97.4545H168.618V98.5455H168.945C169.098 98.1309 169.331 97.8291 169.643 97.64C169.956 97.4436 170.334 97.3455 170.778 97.3455C171.265 97.3455 171.672 97.5091 172 97.8364C172.327 98.1564 172.491 98.6109 172.491 99.2V100.182H171.454V99.5927C171.454 99.1782 171.36 98.8727 171.171 98.6764C170.989 98.48 170.709 98.3818 170.331 98.3818C169.989 98.3818 169.676 98.4655 169.392 98.6327C169.116 98.8 168.858 99.0618 168.618 99.4182V104H167.527ZM176.564 103.171C176.942 103.171 177.291 103.105 177.611 102.975C177.931 102.836 178.208 102.6 178.44 102.265V101.218H176.477C176.164 101.218 175.902 101.291 175.691 101.436C175.488 101.575 175.386 101.822 175.386 102.178C175.386 102.491 175.473 102.735 175.648 102.909C175.829 103.084 176.135 103.171 176.564 103.171ZM176.422 104.109C175.869 104.109 175.375 103.964 174.939 103.673C174.509 103.375 174.295 102.898 174.295 102.244C174.295 101.669 174.48 101.218 174.851 100.891C175.229 100.564 175.669 100.4 176.171 100.4H178.44V99.4618C178.44 99.1127 178.324 98.8364 178.091 98.6327C177.859 98.4291 177.506 98.3273 177.033 98.3273C176.553 98.3273 176.193 98.4 175.953 98.5455C175.72 98.6836 175.535 98.9964 175.397 99.4836L174.437 99.2218C174.56 98.6036 174.837 98.1382 175.266 97.8255C175.702 97.5055 176.284 97.3455 177.011 97.3455C177.731 97.3455 178.331 97.5091 178.811 97.8364C179.291 98.1564 179.531 98.6545 179.531 99.3309V102.255C179.531 102.647 179.64 102.895 179.859 102.996C180.077 103.091 180.404 103.102 180.84 103.029V104.033C180.389 104.098 179.964 104.069 179.564 103.945C179.171 103.815 178.957 103.469 178.92 102.909H178.702C178.499 103.244 178.193 103.527 177.786 103.76C177.379 103.993 176.924 104.109 176.422 104.109ZM185.143 104.109C184.19 104.109 183.47 103.902 182.983 103.487C182.503 103.065 182.263 102.447 182.263 101.633V99.8655C182.263 99.0364 182.503 98.4109 182.983 97.9891C183.47 97.56 184.186 97.3455 185.132 97.3455C185.976 97.3455 186.623 97.5382 187.074 97.9236C187.532 98.3018 187.786 98.8582 187.837 99.5927L186.768 99.6582C186.739 99.2073 186.59 98.8727 186.321 98.6545C186.052 98.4364 185.652 98.3273 185.121 98.3273C184.525 98.3273 184.081 98.4545 183.79 98.7091C183.499 98.9564 183.354 99.3418 183.354 99.8655V101.633C183.354 102.135 183.499 102.509 183.79 102.756C184.088 103.004 184.539 103.127 185.143 103.127C185.666 103.127 186.063 103.036 186.332 102.855C186.601 102.665 186.746 102.385 186.768 102.015L187.837 102.08C187.786 102.749 187.536 103.255 187.085 103.596C186.634 103.938 185.986 104.109 185.143 104.109ZM193.711 104.098C192.991 104.098 192.431 103.92 192.031 103.564C191.631 103.207 191.431 102.662 191.431 101.927V98.4364H189.576V97.4545H191.431V95.2727H192.522V97.4545H195.358V98.4364H192.522V101.829C192.522 102.251 192.627 102.564 192.838 102.767C193.049 102.971 193.362 103.073 193.776 103.073C194.052 103.073 194.311 103.055 194.551 103.018C194.791 102.975 195.02 102.92 195.238 102.855L195.456 103.847C195.209 103.92 194.943 103.978 194.66 104.022C194.383 104.073 194.067 104.098 193.711 104.098ZM197.544 104V103.018H199.726V98.4364H197.544V97.4545H200.817V103.018H202.999V104H197.544ZM200.271 96.2545C199.995 96.2545 199.769 96.2109 199.595 96.1236C199.428 96.0364 199.344 95.8727 199.344 95.6327V95.5564C199.344 95.3091 199.428 95.1418 199.595 95.0545C199.769 94.96 199.995 94.9127 200.271 94.9127C200.548 94.9127 200.769 94.96 200.937 95.0545C201.111 95.1418 201.199 95.3091 201.199 95.5564V95.6327C201.199 95.8727 201.111 96.0364 200.937 96.1236C200.762 96.2109 200.54 96.2545 200.271 96.2545ZM208.588 104H207.225L204.901 97.4545H206.09L207.923 103.116L209.756 97.4545H210.912L208.588 104ZM218.302 102.56C218.105 103.105 217.807 103.502 217.407 103.749C217.007 103.989 216.393 104.109 215.563 104.109C214.698 104.109 214.025 103.902 213.545 103.487C213.065 103.065 212.825 102.444 212.825 101.622V99.8655C212.825 99.0218 213.076 98.3927 213.578 97.9782C214.087 97.5564 214.749 97.3455 215.563 97.3455C216.385 97.3455 217.043 97.56 217.538 97.9891C218.033 98.4109 218.28 99.0618 218.28 99.9418V100.945H213.916V101.633C213.916 102.178 214.051 102.575 214.32 102.822C214.596 103.062 215.007 103.182 215.553 103.182C216.076 103.182 216.458 103.109 216.698 102.964C216.938 102.818 217.12 102.582 217.243 102.255L218.302 102.56ZM213.916 99.8655V100.127H217.189V99.9636C217.189 99.3745 217.051 98.9455 216.774 98.6764C216.505 98.4073 216.098 98.2727 215.553 98.2727C215.007 98.2727 214.596 98.4036 214.32 98.6655C214.051 98.92 213.916 99.32 213.916 99.8655ZM41.1055 122L39.8182 116.796L38.5309 122H37.44L36.2618 113.273H37.3418L38.1273 120.396L39.2727 115.455H40.4182L41.5636 120.396L42.3491 113.273H43.3745L42.1964 122H41.1055ZM44.7315 122V121.018H46.9134V116.436H44.7315V115.455H48.0043V121.018H50.1861V122H44.7315ZM47.4588 114.255C47.1824 114.255 46.957 114.211 46.7824 114.124C46.6152 114.036 46.5315 113.873 46.5315 113.633V113.556C46.5315 113.309 46.6152 113.142 46.7824 113.055C46.957 112.96 47.1824 112.913 47.4588 112.913C47.7352 112.913 47.957 112.96 48.1243 113.055C48.2988 113.142 48.3861 113.309 48.3861 113.556V113.633C48.3861 113.873 48.2988 114.036 48.1243 114.124C47.9497 114.211 47.7279 114.255 47.4588 114.255ZM52.3722 122V115.455H53.4631V116.545H53.7903C53.9503 116.175 54.2013 115.884 54.5431 115.673C54.8849 115.455 55.274 115.345 55.7103 115.345C56.3649 115.345 56.8813 115.545 57.2594 115.945C57.6376 116.345 57.8267 116.891 57.8267 117.582V122H56.7358V117.8C56.7358 117.342 56.6231 116.993 56.3976 116.753C56.1794 116.505 55.8631 116.382 55.4485 116.382C55.1213 116.382 54.7867 116.48 54.4449 116.676C54.1103 116.873 53.7831 117.156 53.4631 117.527V122H52.3722ZM62.4128 121.073C62.7691 121.073 63.1073 120.978 63.4273 120.789C63.7473 120.6 64.0637 120.313 64.3764 119.927V116.72C64.151 116.582 63.911 116.48 63.6564 116.415C63.4019 116.342 63.1291 116.305 62.8382 116.305C62.2491 116.305 61.8128 116.436 61.5291 116.698C61.2455 116.953 61.1037 117.342 61.1037 117.865V119.622C61.1037 120.109 61.2128 120.473 61.431 120.713C61.6491 120.953 61.9764 121.073 62.4128 121.073ZM64.0491 120.909C63.8601 121.287 63.5982 121.582 63.2637 121.793C62.9291 122.004 62.5546 122.109 62.1401 122.109C61.4637 122.109 60.9401 121.891 60.5691 121.455C60.1982 121.018 60.0128 120.407 60.0128 119.622V117.844C60.0128 117.051 60.2601 116.436 60.7546 116C61.2491 115.564 61.9401 115.345 62.8273 115.345C63.0819 115.345 63.3437 115.375 63.6128 115.433C63.8819 115.484 64.1364 115.556 64.3764 115.651V113.273H65.4673V122H64.3764V120.909H64.0491ZM70.3807 122.109C69.5007 122.109 68.8243 121.898 68.3516 121.476C67.8861 121.047 67.6534 120.433 67.6534 119.633V117.865C67.6534 117.058 67.8898 116.436 68.3625 116C68.8352 115.564 69.5116 115.345 70.3916 115.345C71.2643 115.345 71.9334 115.564 72.3989 116C72.8716 116.436 73.108 117.058 73.108 117.865V119.633C73.108 120.433 72.8716 121.047 72.3989 121.476C71.9334 121.898 71.2607 122.109 70.3807 122.109ZM70.3807 121.127C70.9334 121.127 71.3443 121.004 71.6134 120.756C71.8825 120.509 72.017 120.135 72.017 119.633V117.865C72.017 117.342 71.8825 116.956 71.6134 116.709C71.3443 116.455 70.9334 116.327 70.3807 116.327C69.828 116.327 69.417 116.455 69.148 116.709C68.8789 116.956 68.7443 117.342 68.7443 117.865V119.633C68.7443 120.135 68.8789 120.509 69.148 120.756C69.417 121.004 69.828 121.127 70.3807 121.127ZM78.9813 122L78.0213 117.735L77.1049 122H75.8613L74.6286 115.455H75.6759L76.5049 121.204L77.3559 116.545H78.6431L79.5704 121.029L80.3668 115.455H81.414L80.1813 122H78.9813ZM85.2256 118.727L84.8983 113.273H86.4256L86.0983 118.727H85.2256ZM85.6728 122.109C85.3092 122.109 85.0401 122.036 84.8656 121.891C84.6983 121.738 84.6147 121.505 84.6147 121.193V120.887C84.6147 120.567 84.6983 120.331 84.8656 120.178C85.0401 120.025 85.3092 119.949 85.6728 119.949C86.0365 119.949 86.3019 120.025 86.4692 120.178C86.6365 120.331 86.7201 120.567 86.7201 120.887V121.193C86.7201 121.505 86.6328 121.738 86.4583 121.891C86.291 122.036 86.0292 122.109 85.6728 122.109ZM94.3935 117.636L94.0662 113.273H95.8116L95.4844 117.636H94.3935ZM91.1207 117.636L90.7935 113.273H92.5389L92.2116 117.636H91.1207Z" fill="var(--vscode-debugTokenExpression-string, #CE9178)" /> + <path d="M39.72 193.149C40.3309 193.149 40.7709 193.025 41.04 192.778C41.3164 192.531 41.4545 192.135 41.4545 191.589V189.822C41.4545 189.32 41.3491 188.956 41.1382 188.731C40.9345 188.498 40.6036 188.382 40.1455 188.382C39.7818 188.382 39.4436 188.473 39.1309 188.655C38.8182 188.836 38.5018 189.127 38.1818 189.527V192.735C38.4073 192.873 38.64 192.978 38.88 193.051C39.1273 193.116 39.4073 193.149 39.72 193.149ZM39.7309 194.109C39.4836 194.109 39.2218 194.08 38.9455 194.022C38.6764 193.971 38.4218 193.898 38.1818 193.804V196.182H37.0909V187.455H38.1818V188.545H38.5091C38.6909 188.175 38.9527 187.884 39.2945 187.673C39.6436 187.455 40.0182 187.345 40.4182 187.345C41.0727 187.345 41.5891 187.571 41.9673 188.022C42.3527 188.465 42.5455 189.069 42.5455 189.833V191.611C42.5455 192.382 42.2909 192.993 41.7818 193.444C41.28 193.887 40.5964 194.109 39.7309 194.109ZM45.277 194V187.455H46.3679V188.545H46.6952C46.8479 188.131 47.0806 187.829 47.3934 187.64C47.7061 187.444 48.0843 187.345 48.5279 187.345C49.0152 187.345 49.4224 187.509 49.7497 187.836C50.077 188.156 50.2406 188.611 50.2406 189.2V190.182H49.2043V189.593C49.2043 189.178 49.1097 188.873 48.9206 188.676C48.7388 188.48 48.4588 188.382 48.0806 188.382C47.7388 188.382 47.4261 188.465 47.1424 188.633C46.8661 188.8 46.6079 189.062 46.3679 189.418V194H45.277ZM52.3722 194V193.018H54.554V188.436H52.3722V187.455H55.6449V193.018H57.8267V194H52.3722ZM55.0994 186.255C54.8231 186.255 54.5976 186.211 54.4231 186.124C54.2558 186.036 54.1722 185.873 54.1722 185.633V185.556C54.1722 185.309 54.2558 185.142 54.4231 185.055C54.5976 184.96 54.8231 184.913 55.0994 184.913C55.3758 184.913 55.5976 184.96 55.7649 185.055C55.9394 185.142 56.0267 185.309 56.0267 185.556V185.633C56.0267 185.873 55.9394 186.036 55.7649 186.124C55.5903 186.211 55.3685 186.255 55.0994 186.255ZM60.0128 194V187.455H61.1037V188.545H61.431C61.591 188.175 61.8419 187.884 62.1837 187.673C62.5255 187.455 62.9146 187.345 63.351 187.345C64.0055 187.345 64.5219 187.545 64.9001 187.945C65.2782 188.345 65.4673 188.891 65.4673 189.582V194H64.3764V189.8C64.3764 189.342 64.2637 188.993 64.0382 188.753C63.8201 188.505 63.5037 188.382 63.0891 188.382C62.7619 188.382 62.4273 188.48 62.0855 188.676C61.751 188.873 61.4237 189.156 61.1037 189.527V194H60.0128ZM71.4607 194.098C70.7407 194.098 70.1807 193.92 69.7807 193.564C69.3807 193.207 69.1807 192.662 69.1807 191.927V188.436H67.3261V187.455H69.1807V185.273H70.2716V187.455H73.108V188.436H70.2716V191.829C70.2716 192.251 70.377 192.564 70.588 192.767C70.7989 192.971 71.1116 193.073 71.5261 193.073C71.8025 193.073 72.0607 193.055 72.3007 193.018C72.5407 192.975 72.7698 192.92 72.988 192.855L73.2061 193.847C72.9589 193.92 72.6934 193.978 72.4098 194.022C72.1334 194.073 71.817 194.098 71.4607 194.098Z" fill="#DCDCAA" /> + <rect x="36" y="157" width="211" height="2" fill="var(--vscode-focusBorder, #3794FF)" /> + <rect x="36" y="205" width="211" height="2" fill="var(--vscode-focusBorder, #3794FF)" /> + <path d="M286.658 96.3091V101H285.655V94.4545H286.658V95.3273H286.876C287.015 95.0218 287.196 94.7818 287.422 94.6073C287.647 94.4327 287.884 94.3455 288.131 94.3455C288.415 94.3455 288.651 94.4327 288.84 94.6073C289.036 94.7745 289.171 95.0145 289.244 95.3273H289.484C289.622 95.0218 289.807 94.7818 290.04 94.6073C290.28 94.4327 290.535 94.3455 290.804 94.3455C291.167 94.3455 291.455 94.4945 291.665 94.7927C291.876 95.0836 291.982 95.48 291.982 95.9818V101H290.978V96.1564C290.978 95.8291 290.935 95.5818 290.847 95.4145C290.76 95.2473 290.633 95.1636 290.465 95.1636C290.327 95.1636 290.153 95.2655 289.942 95.4691C289.738 95.6727 289.531 95.9527 289.32 96.3091V101H288.316V96.1564C288.316 95.8291 288.269 95.5818 288.175 95.4145C288.087 95.2473 287.96 95.1636 287.793 95.1636C287.647 95.1636 287.476 95.2618 287.28 95.4582C287.091 95.6545 286.884 95.9382 286.658 96.3091ZM296.546 101.109C295.993 101.109 295.459 101.029 294.942 100.869C294.426 100.702 293.95 100.447 293.513 100.105L294.004 99.2436C294.361 99.5345 294.757 99.7564 295.193 99.9091C295.637 100.055 296.088 100.127 296.546 100.127C296.924 100.127 297.284 100.069 297.626 99.9527C297.975 99.8364 298.15 99.5891 298.15 99.2109C298.15 98.8982 298.008 98.6764 297.724 98.5455C297.441 98.4145 296.921 98.3018 296.164 98.2073C295.364 98.1127 294.757 97.9273 294.342 97.6509C293.935 97.3673 293.732 96.9236 293.732 96.32C293.732 95.6655 293.979 95.1745 294.473 94.8473C294.968 94.5127 295.542 94.3455 296.197 94.3455C296.713 94.3455 297.208 94.4327 297.681 94.6073C298.161 94.7818 298.604 95.0291 299.012 95.3491L298.521 96.2109C298.186 95.9418 297.822 95.7273 297.43 95.5673C297.037 95.4073 296.626 95.3273 296.197 95.3273C295.848 95.3273 295.528 95.3927 295.237 95.5236C294.953 95.6545 294.812 95.8945 294.812 96.2436C294.812 96.5418 294.942 96.76 295.204 96.8982C295.473 97.0291 295.986 97.1418 296.742 97.2364C297.572 97.3309 298.193 97.5345 298.608 97.8473C299.022 98.1527 299.23 98.5818 299.23 99.1345C299.23 99.8182 298.953 100.32 298.401 100.64C297.848 100.953 297.23 101.109 296.546 101.109ZM307.372 101.524C307.372 102.113 307.154 102.553 306.718 102.844C306.289 103.142 305.761 103.291 305.136 103.291H303.085C302.43 103.291 301.889 103.16 301.459 102.898C301.038 102.636 300.827 102.233 300.827 101.687C300.827 101.338 300.918 101.062 301.099 100.858C301.289 100.647 301.561 100.527 301.918 100.498V100.269C301.663 100.225 301.452 100.12 301.285 99.9527C301.125 99.7855 301.045 99.5273 301.045 99.1782C301.045 98.8655 301.143 98.5855 301.339 98.3382C301.536 98.0836 301.867 97.9382 302.332 97.9018V97.6945C301.976 97.5782 301.736 97.4036 301.612 97.1709C301.489 96.9309 301.427 96.7018 301.427 96.4836V96.1891C301.427 95.6873 301.641 95.2727 302.07 94.9455C302.499 94.6182 303.103 94.4545 303.881 94.4545H307.099V95.4364L305.409 95.1855V95.3818C305.699 95.4618 305.918 95.6145 306.063 95.84C306.216 96.0655 306.292 96.2909 306.292 96.5164V96.8C306.292 97.3018 306.099 97.7091 305.714 98.0218C305.329 98.3273 304.703 98.48 303.838 98.48H302.518C302.321 98.48 302.172 98.5309 302.07 98.6327C301.969 98.7273 301.918 98.8909 301.918 99.1236C301.918 99.3491 301.987 99.52 302.125 99.6364C302.27 99.7455 302.485 99.8 302.769 99.8H305.245C305.856 99.8 306.361 99.9345 306.761 100.204C307.169 100.473 307.372 100.913 307.372 101.524ZM303.881 97.6509C304.325 97.6509 304.663 97.5709 304.896 97.4109C305.129 97.2509 305.245 97 305.245 96.6582V96.3091C305.245 95.9673 305.129 95.72 304.896 95.5673C304.663 95.4073 304.325 95.3273 303.881 95.3273C303.438 95.3273 303.099 95.4073 302.867 95.5673C302.634 95.72 302.518 95.9673 302.518 96.3091V96.6582C302.518 97 302.634 97.2509 302.867 97.4109C303.099 97.5709 303.438 97.6509 303.881 97.6509ZM303.063 102.418H305.278C305.605 102.418 305.87 102.356 306.074 102.233C306.278 102.109 306.379 101.895 306.379 101.589C306.379 101.298 306.285 101.091 306.096 100.967C305.914 100.844 305.678 100.782 305.387 100.782H301.918V101.524C301.918 101.822 302.016 102.044 302.212 102.189C302.409 102.342 302.692 102.418 303.063 102.418ZM316.326 96.2V95.1091H322.435V96.2H316.326ZM316.326 99.2545V98.1636H322.435V99.2545H316.326Z" fill="var(--vscode-editor-foreground, #D4D4D4)" /> + <path d="M335.753 96.6364L335.426 92.2727H337.171L336.844 96.6364H335.753ZM332.48 96.6364L332.153 92.2727H333.898L333.571 96.6364H332.48ZM339.575 101V92.2727H340.666V95.7636H343.939V92.2727H345.03V101H343.939V96.7455H340.666V101H339.575ZM352.692 99.56C352.496 100.105 352.198 100.502 351.798 100.749C351.398 100.989 350.783 101.109 349.954 101.109C349.089 101.109 348.416 100.902 347.936 100.487C347.456 100.065 347.216 99.4436 347.216 98.6218V96.8655C347.216 96.0218 347.467 95.3927 347.969 94.9782C348.478 94.5564 349.14 94.3455 349.954 94.3455C350.776 94.3455 351.434 94.56 351.929 94.9891C352.423 95.4109 352.67 96.0618 352.67 96.9418V97.9455H348.307V98.6327C348.307 99.1782 348.441 99.5745 348.71 99.8218C348.987 100.062 349.398 100.182 349.943 100.182C350.467 100.182 350.849 100.109 351.089 99.9636C351.329 99.8182 351.51 99.5818 351.634 99.2545L352.692 99.56ZM348.307 96.8655V97.1273H351.58V96.9636C351.58 96.3745 351.441 95.9455 351.165 95.6764C350.896 95.4073 350.489 95.2727 349.943 95.2727C349.398 95.2727 348.987 95.4036 348.71 95.6655C348.441 95.92 348.307 96.32 348.307 96.8655ZM354.857 101V100.018H357.038V93.2545H354.857V92.2727H358.129V100.018H360.311V101H354.857ZM362.497 101V100.018H364.679V93.2545H362.497V92.2727H365.77V100.018H367.952V101H362.497ZM372.865 101.109C371.985 101.109 371.309 100.898 370.836 100.476C370.371 100.047 370.138 99.4327 370.138 98.6327V96.8655C370.138 96.0582 370.374 95.4364 370.847 95C371.32 94.5636 371.996 94.3455 372.876 94.3455C373.749 94.3455 374.418 94.5636 374.883 95C375.356 95.4364 375.592 96.0582 375.592 96.8655V98.6327C375.592 99.4327 375.356 100.047 374.883 100.476C374.418 100.898 373.745 101.109 372.865 101.109ZM372.865 100.127C373.418 100.127 373.829 100.004 374.098 99.7564C374.367 99.5091 374.501 99.1345 374.501 98.6327V96.8655C374.501 96.3418 374.367 95.9564 374.098 95.7091C373.829 95.4545 373.418 95.3273 372.865 95.3273C372.312 95.3273 371.901 95.4545 371.632 95.7091C371.363 95.9564 371.229 96.3418 371.229 96.8655V98.6327C371.229 99.1345 371.363 99.5091 371.632 99.7564C371.901 100.004 372.312 100.127 372.865 100.127ZM385.419 101V100.018H387.601V93.2545H385.419V92.2727H390.874V93.2545H388.692V100.018H390.874V101H385.419ZM393.06 101V94.4545H394.151V95.5455H394.478C394.638 95.1745 394.889 94.8836 395.231 94.6727C395.572 94.4545 395.961 94.3455 396.398 94.3455C397.052 94.3455 397.569 94.5455 397.947 94.9455C398.325 95.3455 398.514 95.8909 398.514 96.5818V101H397.423V96.8C397.423 96.3418 397.311 95.9927 397.085 95.7527C396.867 95.5055 396.551 95.3818 396.136 95.3818C395.809 95.3818 395.474 95.48 395.132 95.6764C394.798 95.8727 394.471 96.1564 394.151 96.5273V101H393.06ZM404.508 101.098C403.788 101.098 403.228 100.92 402.828 100.564C402.428 100.207 402.228 99.6618 402.228 98.9273V95.4364H400.373V94.4545H402.228V92.2727H403.318V94.4545H406.155V95.4364H403.318V98.8291C403.318 99.2509 403.424 99.5636 403.635 99.7673C403.846 99.9709 404.158 100.073 404.573 100.073C404.849 100.073 405.108 100.055 405.348 100.018C405.588 99.9745 405.817 99.92 406.035 99.8545L406.253 100.847C406.006 100.92 405.74 100.978 405.457 101.022C405.18 101.073 404.864 101.098 404.508 101.098ZM413.817 99.56C413.621 100.105 413.323 100.502 412.923 100.749C412.523 100.989 411.908 101.109 411.079 101.109C410.214 101.109 409.541 100.902 409.061 100.487C408.581 100.065 408.341 99.4436 408.341 98.6218V96.8655C408.341 96.0218 408.592 95.3927 409.094 94.9782C409.603 94.5564 410.265 94.3455 411.079 94.3455C411.901 94.3455 412.559 94.56 413.054 94.9891C413.548 95.4109 413.795 96.0618 413.795 96.9418V97.9455H409.432V98.6327C409.432 99.1782 409.566 99.5745 409.835 99.8218C410.112 100.062 410.523 100.182 411.068 100.182C411.592 100.182 411.974 100.109 412.214 99.9636C412.454 99.8182 412.635 99.5818 412.759 99.2545L413.817 99.56ZM409.432 96.8655V97.1273H412.705V96.9636C412.705 96.3745 412.566 95.9455 412.29 95.6764C412.021 95.4073 411.614 95.2727 411.068 95.2727C410.523 95.2727 410.112 95.4036 409.835 95.6655C409.566 95.92 409.432 96.32 409.432 96.8655ZM416.527 101V94.4545H417.618V95.5455H417.945C418.098 95.1309 418.331 94.8291 418.643 94.64C418.956 94.4436 419.334 94.3455 419.778 94.3455C420.265 94.3455 420.672 94.5091 421 94.8364C421.327 95.1564 421.491 95.6109 421.491 96.2V97.1818H420.454V96.5927C420.454 96.1782 420.36 95.8727 420.171 95.6764C419.989 95.48 419.709 95.3818 419.331 95.3818C418.989 95.3818 418.676 95.4655 418.392 95.6327C418.116 95.8 417.858 96.0618 417.618 96.4182V101H416.527ZM425.564 100.171C425.942 100.171 426.291 100.105 426.611 99.9745C426.931 99.8364 427.208 99.6 427.44 99.2655V98.2182H425.477C425.164 98.2182 424.902 98.2909 424.691 98.4364C424.488 98.5745 424.386 98.8218 424.386 99.1782C424.386 99.4909 424.473 99.7345 424.648 99.9091C424.829 100.084 425.135 100.171 425.564 100.171ZM425.422 101.109C424.869 101.109 424.375 100.964 423.939 100.673C423.509 100.375 423.295 99.8982 423.295 99.2436C423.295 98.6691 423.48 98.2182 423.851 97.8909C424.229 97.5636 424.669 97.4 425.171 97.4H427.44V96.4618C427.44 96.1127 427.324 95.8364 427.091 95.6327C426.859 95.4291 426.506 95.3273 426.033 95.3273C425.553 95.3273 425.193 95.4 424.953 95.5455C424.72 95.6836 424.535 95.9964 424.397 96.4836L423.437 96.2218C423.56 95.6036 423.837 95.1382 424.266 94.8255C424.702 94.5055 425.284 94.3455 426.011 94.3455C426.731 94.3455 427.331 94.5091 427.811 94.8364C428.291 95.1564 428.531 95.6545 428.531 96.3309V99.2545C428.531 99.6473 428.64 99.8945 428.859 99.9964C429.077 100.091 429.404 100.102 429.84 100.029V101.033C429.389 101.098 428.964 101.069 428.564 100.945C428.171 100.815 427.957 100.469 427.92 99.9091H427.702C427.499 100.244 427.193 100.527 426.786 100.76C426.379 100.993 425.924 101.109 425.422 101.109ZM434.143 101.109C433.19 101.109 432.47 100.902 431.983 100.487C431.503 100.065 431.263 99.4473 431.263 98.6327V96.8655C431.263 96.0364 431.503 95.4109 431.983 94.9891C432.47 94.56 433.186 94.3455 434.132 94.3455C434.976 94.3455 435.623 94.5382 436.074 94.9236C436.532 95.3018 436.786 95.8582 436.837 96.5927L435.768 96.6582C435.739 96.2073 435.59 95.8727 435.321 95.6545C435.052 95.4364 434.652 95.3273 434.121 95.3273C433.525 95.3273 433.081 95.4545 432.79 95.7091C432.499 95.9564 432.354 96.3418 432.354 96.8655V98.6327C432.354 99.1345 432.499 99.5091 432.79 99.7564C433.088 100.004 433.539 100.127 434.143 100.127C434.666 100.127 435.063 100.036 435.332 99.8545C435.601 99.6655 435.746 99.3855 435.768 99.0145L436.837 99.08C436.786 99.7491 436.536 100.255 436.085 100.596C435.634 100.938 434.986 101.109 434.143 101.109ZM442.711 101.098C441.991 101.098 441.431 100.92 441.031 100.564C440.631 100.207 440.431 99.6618 440.431 98.9273V95.4364H438.576V94.4545H440.431V92.2727H441.522V94.4545H444.358V95.4364H441.522V98.8291C441.522 99.2509 441.627 99.5636 441.838 99.7673C442.049 99.9709 442.362 100.073 442.776 100.073C443.052 100.073 443.311 100.055 443.551 100.018C443.791 99.9745 444.02 99.92 444.238 99.8545L444.456 100.847C444.209 100.92 443.943 100.978 443.66 101.022C443.383 101.073 443.067 101.098 442.711 101.098ZM446.544 101V100.018H448.726V95.4364H446.544V94.4545H449.817V100.018H451.999V101H446.544ZM449.271 93.2545C448.995 93.2545 448.769 93.2109 448.595 93.1236C448.428 93.0364 448.344 92.8727 448.344 92.6327V92.5564C448.344 92.3091 448.428 92.1418 448.595 92.0545C448.769 91.96 448.995 91.9127 449.271 91.9127C449.548 91.9127 449.769 91.96 449.937 92.0545C450.111 92.1418 450.199 92.3091 450.199 92.5564V92.6327C450.199 92.8727 450.111 93.0364 449.937 93.1236C449.762 93.2109 449.54 93.2545 449.271 93.2545ZM457.588 101H456.225L453.901 94.4545H455.09L456.923 100.116L458.756 94.4545H459.912L457.588 101ZM467.302 99.56C467.105 100.105 466.807 100.502 466.407 100.749C466.007 100.989 465.393 101.109 464.563 101.109C463.698 101.109 463.025 100.902 462.545 100.487C462.065 100.065 461.825 99.4436 461.825 98.6218V96.8655C461.825 96.0218 462.076 95.3927 462.578 94.9782C463.087 94.5564 463.749 94.3455 464.563 94.3455C465.385 94.3455 466.043 94.56 466.538 94.9891C467.033 95.4109 467.28 96.0618 467.28 96.9418V97.9455H462.916V98.6327C462.916 99.1782 463.051 99.5745 463.32 99.8218C463.596 100.062 464.007 100.182 464.553 100.182C465.076 100.182 465.458 100.109 465.698 99.9636C465.938 99.8182 466.12 99.5818 466.243 99.2545L467.302 99.56ZM462.916 96.8655V97.1273H466.189V96.9636C466.189 96.3745 466.051 95.9455 465.774 95.6764C465.505 95.4073 465.098 95.2727 464.553 95.2727C464.007 95.2727 463.596 95.4036 463.32 95.6655C463.051 95.92 462.916 96.32 462.916 96.8655ZM290.105 119L288.818 113.796L287.531 119H286.44L285.262 110.273H286.342L287.127 117.396L288.273 112.455H289.418L290.564 117.396L291.349 110.273H292.375L291.196 119H290.105ZM293.732 119V118.018H295.913V113.436H293.732V112.455H297.004V118.018H299.186V119H293.732ZM296.459 111.255C296.182 111.255 295.957 111.211 295.782 111.124C295.615 111.036 295.532 110.873 295.532 110.633V110.556C295.532 110.309 295.615 110.142 295.782 110.055C295.957 109.96 296.182 109.913 296.459 109.913C296.735 109.913 296.957 109.96 297.124 110.055C297.299 110.142 297.386 110.309 297.386 110.556V110.633C297.386 110.873 297.299 111.036 297.124 111.124C296.95 111.211 296.728 111.255 296.459 111.255ZM301.372 119V112.455H302.463V113.545H302.79C302.95 113.175 303.201 112.884 303.543 112.673C303.885 112.455 304.274 112.345 304.71 112.345C305.365 112.345 305.881 112.545 306.259 112.945C306.638 113.345 306.827 113.891 306.827 114.582V119H305.736V114.8C305.736 114.342 305.623 113.993 305.398 113.753C305.179 113.505 304.863 113.382 304.449 113.382C304.121 113.382 303.787 113.48 303.445 113.676C303.11 113.873 302.783 114.156 302.463 114.527V119H301.372ZM311.413 118.073C311.769 118.073 312.107 117.978 312.427 117.789C312.747 117.6 313.064 117.313 313.376 116.927V113.72C313.151 113.582 312.911 113.48 312.656 113.415C312.402 113.342 312.129 113.305 311.838 113.305C311.249 113.305 310.813 113.436 310.529 113.698C310.246 113.953 310.104 114.342 310.104 114.865V116.622C310.104 117.109 310.213 117.473 310.431 117.713C310.649 117.953 310.976 118.073 311.413 118.073ZM313.049 117.909C312.86 118.287 312.598 118.582 312.264 118.793C311.929 119.004 311.555 119.109 311.14 119.109C310.464 119.109 309.94 118.891 309.569 118.455C309.198 118.018 309.013 117.407 309.013 116.622V114.844C309.013 114.051 309.26 113.436 309.755 113C310.249 112.564 310.94 112.345 311.827 112.345C312.082 112.345 312.344 112.375 312.613 112.433C312.882 112.484 313.136 112.556 313.376 112.651V110.273H314.467V119H313.376V117.909H313.049ZM319.381 119.109C318.501 119.109 317.824 118.898 317.352 118.476C316.886 118.047 316.653 117.433 316.653 116.633V114.865C316.653 114.058 316.89 113.436 317.363 113C317.835 112.564 318.512 112.345 319.392 112.345C320.264 112.345 320.933 112.564 321.399 113C321.872 113.436 322.108 114.058 322.108 114.865V116.633C322.108 117.433 321.872 118.047 321.399 118.476C320.933 118.898 320.261 119.109 319.381 119.109ZM319.381 118.127C319.933 118.127 320.344 118.004 320.613 117.756C320.883 117.509 321.017 117.135 321.017 116.633V114.865C321.017 114.342 320.883 113.956 320.613 113.709C320.344 113.455 319.933 113.327 319.381 113.327C318.828 113.327 318.417 113.455 318.148 113.709C317.879 113.956 317.744 114.342 317.744 114.865V116.633C317.744 117.135 317.879 117.509 318.148 117.756C318.417 118.004 318.828 118.127 319.381 118.127ZM327.981 119L327.021 114.735L326.105 119H324.861L323.629 112.455H324.676L325.505 118.204L326.356 113.545H327.643L328.57 118.029L329.367 112.455H330.414L329.181 119H327.981ZM334.226 115.727L333.898 110.273H335.426L335.098 115.727H334.226ZM334.673 119.109C334.309 119.109 334.04 119.036 333.866 118.891C333.698 118.738 333.615 118.505 333.615 118.193V117.887C333.615 117.567 333.698 117.331 333.866 117.178C334.04 117.025 334.309 116.949 334.673 116.949C335.036 116.949 335.302 117.025 335.469 117.178C335.636 117.331 335.72 117.567 335.72 117.887V118.193C335.72 118.505 335.633 118.738 335.458 118.891C335.291 119.036 335.029 119.109 334.673 119.109ZM343.393 114.636L343.066 110.273H344.812L344.484 114.636H343.393ZM340.121 114.636L339.793 110.273H341.539L341.212 114.636H340.121Z" fill="var(--vscode-debugTokenExpression-string, #CE9178)" /> + <path d="M349.954 119.109C349.59 119.109 349.296 119.029 349.07 118.869C348.845 118.702 348.732 118.44 348.732 118.084V117.778C348.732 117.422 348.845 117.16 349.07 116.993C349.296 116.818 349.59 116.731 349.954 116.731C350.318 116.731 350.609 116.818 350.827 116.993C351.052 117.16 351.165 117.422 351.165 117.778V118.084C351.165 118.44 351.052 118.702 350.827 118.869C350.601 119.029 350.31 119.109 349.954 119.109ZM354.595 119.109C354.231 119.109 353.937 119.029 353.711 118.869C353.486 118.702 353.373 118.44 353.373 118.084V117.778C353.373 117.422 353.486 117.16 353.711 116.993C353.937 116.818 354.231 116.731 354.595 116.731C354.958 116.731 355.249 116.818 355.467 116.993C355.693 117.16 355.806 117.422 355.806 117.778V118.084C355.806 118.44 355.693 118.702 355.467 118.869C355.242 119.029 354.951 119.109 354.595 119.109ZM359.235 119.109C358.872 119.109 358.577 119.029 358.352 118.869C358.126 118.702 358.014 118.44 358.014 118.084V117.778C358.014 117.422 358.126 117.16 358.352 116.993C358.577 116.818 358.872 116.731 359.235 116.731C359.599 116.731 359.89 116.818 360.108 116.993C360.334 117.16 360.446 117.422 360.446 117.778V118.084C360.446 118.44 360.334 118.702 360.108 118.869C359.883 119.029 359.592 119.109 359.235 119.109Z" fill="var(--vscode-editorCodeLens-foreground, #808080)" /> + <rect x="276" y="128" width="226" height="1" fill="var(--vscode-editorWidget-border, #3A3D41)" /> + <rect x="276" y="57" width="32" height="8" rx="4" fill="var(--vscode-editorOverviewRuler-commonContentForeground, #606060)" /> + <rect x="316" y="57" width="40" height="8" rx="4" fill="var(--vscode-editorOverviewRuler-commonContentForeground, #606060)" /> + <rect x="364" y="57" width="36" height="8" rx="4" fill="var(--vscode-editorOverviewRuler-commonContentForeground, #606060)" /> + <path d="M288.72 173.149C289.331 173.149 289.771 173.025 290.04 172.778C290.316 172.531 290.455 172.135 290.455 171.589V169.822C290.455 169.32 290.349 168.956 290.138 168.731C289.935 168.498 289.604 168.382 289.145 168.382C288.782 168.382 288.444 168.473 288.131 168.655C287.818 168.836 287.502 169.127 287.182 169.527V172.735C287.407 172.873 287.64 172.978 287.88 173.051C288.127 173.116 288.407 173.149 288.72 173.149ZM288.731 174.109C288.484 174.109 288.222 174.08 287.945 174.022C287.676 173.971 287.422 173.898 287.182 173.804V176.182H286.091V167.455H287.182V168.545H287.509C287.691 168.175 287.953 167.884 288.295 167.673C288.644 167.455 289.018 167.345 289.418 167.345C290.073 167.345 290.589 167.571 290.967 168.022C291.353 168.465 291.545 169.069 291.545 169.833V171.611C291.545 172.382 291.291 172.993 290.782 173.444C290.28 173.887 289.596 174.109 288.731 174.109ZM294.277 174V167.455H295.368V168.545H295.695C295.848 168.131 296.081 167.829 296.393 167.64C296.706 167.444 297.084 167.345 297.528 167.345C298.015 167.345 298.422 167.509 298.75 167.836C299.077 168.156 299.241 168.611 299.241 169.2V170.182H298.204V169.593C298.204 169.178 298.11 168.873 297.921 168.676C297.739 168.48 297.459 168.382 297.081 168.382C296.739 168.382 296.426 168.465 296.142 168.633C295.866 168.8 295.608 169.062 295.368 169.418V174H294.277ZM301.372 174V173.018H303.554V168.436H301.372V167.455H304.645V173.018H306.827V174H301.372ZM304.099 166.255C303.823 166.255 303.598 166.211 303.423 166.124C303.256 166.036 303.172 165.873 303.172 165.633V165.556C303.172 165.309 303.256 165.142 303.423 165.055C303.598 164.96 303.823 164.913 304.099 164.913C304.376 164.913 304.598 164.96 304.765 165.055C304.939 165.142 305.027 165.309 305.027 165.556V165.633C305.027 165.873 304.939 166.036 304.765 166.124C304.59 166.211 304.369 166.255 304.099 166.255ZM309.013 174V167.455H310.104V168.545H310.431C310.591 168.175 310.842 167.884 311.184 167.673C311.526 167.455 311.915 167.345 312.351 167.345C313.006 167.345 313.522 167.545 313.9 167.945C314.278 168.345 314.467 168.891 314.467 169.582V174H313.376V169.8C313.376 169.342 313.264 168.993 313.038 168.753C312.82 168.505 312.504 168.382 312.089 168.382C311.762 168.382 311.427 168.48 311.086 168.676C310.751 168.873 310.424 169.156 310.104 169.527V174H309.013ZM320.461 174.098C319.741 174.098 319.181 173.92 318.781 173.564C318.381 173.207 318.181 172.662 318.181 171.927V168.436H316.326V167.455H318.181V165.273H319.272V167.455H322.108V168.436H319.272V171.829C319.272 172.251 319.377 172.564 319.588 172.767C319.799 172.971 320.112 173.073 320.526 173.073C320.802 173.073 321.061 173.055 321.301 173.018C321.541 172.975 321.77 172.92 321.988 172.855L322.206 173.847C321.959 173.92 321.693 173.978 321.41 174.022C321.133 174.073 320.817 174.098 320.461 174.098Z" fill="#DCDCAA" /> + <path d="M329.203 176.291C328.061 176.022 327.138 175.4 326.432 174.425C325.734 173.451 325.385 172.218 325.385 170.727C325.385 169.236 325.734 168.004 326.432 167.029C327.138 166.055 328.061 165.433 329.203 165.164L329.312 166.015C328.338 166.284 327.621 166.847 327.163 167.705C326.705 168.556 326.476 169.564 326.476 170.727C326.476 171.891 326.705 172.902 327.163 173.76C327.621 174.611 328.338 175.171 329.312 175.44L329.203 176.291ZM332.502 169.309V174H331.498V167.455H332.502V168.327H332.72C332.858 168.022 333.04 167.782 333.266 167.607C333.491 167.433 333.727 167.345 333.975 167.345C334.258 167.345 334.495 167.433 334.684 167.607C334.88 167.775 335.015 168.015 335.087 168.327H335.327C335.466 168.022 335.651 167.782 335.884 167.607C336.124 167.433 336.378 167.345 336.647 167.345C337.011 167.345 337.298 167.495 337.509 167.793C337.72 168.084 337.826 168.48 337.826 168.982V174H336.822V169.156C336.822 168.829 336.778 168.582 336.691 168.415C336.604 168.247 336.476 168.164 336.309 168.164C336.171 168.164 335.996 168.265 335.786 168.469C335.582 168.673 335.375 168.953 335.164 169.309V174H334.16V169.156C334.16 168.829 334.113 168.582 334.018 168.415C333.931 168.247 333.804 168.164 333.636 168.164C333.491 168.164 333.32 168.262 333.124 168.458C332.935 168.655 332.727 168.938 332.502 169.309ZM342.39 174.109C341.837 174.109 341.303 174.029 340.786 173.869C340.27 173.702 339.793 173.447 339.357 173.105L339.848 172.244C340.204 172.535 340.601 172.756 341.037 172.909C341.481 173.055 341.932 173.127 342.39 173.127C342.768 173.127 343.128 173.069 343.47 172.953C343.819 172.836 343.993 172.589 343.993 172.211C343.993 171.898 343.852 171.676 343.568 171.545C343.284 171.415 342.764 171.302 342.008 171.207C341.208 171.113 340.601 170.927 340.186 170.651C339.779 170.367 339.575 169.924 339.575 169.32C339.575 168.665 339.823 168.175 340.317 167.847C340.812 167.513 341.386 167.345 342.041 167.345C342.557 167.345 343.052 167.433 343.524 167.607C344.004 167.782 344.448 168.029 344.855 168.349L344.364 169.211C344.03 168.942 343.666 168.727 343.273 168.567C342.881 168.407 342.47 168.327 342.041 168.327C341.692 168.327 341.372 168.393 341.081 168.524C340.797 168.655 340.655 168.895 340.655 169.244C340.655 169.542 340.786 169.76 341.048 169.898C341.317 170.029 341.83 170.142 342.586 170.236C343.415 170.331 344.037 170.535 344.452 170.847C344.866 171.153 345.073 171.582 345.073 172.135C345.073 172.818 344.797 173.32 344.244 173.64C343.692 173.953 343.073 174.109 342.39 174.109ZM353.216 174.524C353.216 175.113 352.998 175.553 352.561 175.844C352.132 176.142 351.605 176.291 350.98 176.291H348.929C348.274 176.291 347.732 176.16 347.303 175.898C346.881 175.636 346.67 175.233 346.67 174.687C346.67 174.338 346.761 174.062 346.943 173.858C347.132 173.647 347.405 173.527 347.761 173.498V173.269C347.507 173.225 347.296 173.12 347.129 172.953C346.969 172.785 346.889 172.527 346.889 172.178C346.889 171.865 346.987 171.585 347.183 171.338C347.38 171.084 347.71 170.938 348.176 170.902V170.695C347.82 170.578 347.58 170.404 347.456 170.171C347.332 169.931 347.27 169.702 347.27 169.484V169.189C347.27 168.687 347.485 168.273 347.914 167.945C348.343 167.618 348.947 167.455 349.725 167.455H352.943V168.436L351.252 168.185V168.382C351.543 168.462 351.761 168.615 351.907 168.84C352.06 169.065 352.136 169.291 352.136 169.516V169.8C352.136 170.302 351.943 170.709 351.558 171.022C351.172 171.327 350.547 171.48 349.681 171.48H348.361C348.165 171.48 348.016 171.531 347.914 171.633C347.812 171.727 347.761 171.891 347.761 172.124C347.761 172.349 347.83 172.52 347.969 172.636C348.114 172.745 348.329 172.8 348.612 172.8H351.089C351.7 172.8 352.205 172.935 352.605 173.204C353.012 173.473 353.216 173.913 353.216 174.524ZM349.725 170.651C350.169 170.651 350.507 170.571 350.74 170.411C350.972 170.251 351.089 170 351.089 169.658V169.309C351.089 168.967 350.972 168.72 350.74 168.567C350.507 168.407 350.169 168.327 349.725 168.327C349.281 168.327 348.943 168.407 348.71 168.567C348.478 168.72 348.361 168.967 348.361 169.309V169.658C348.361 170 348.478 170.251 348.71 170.411C348.943 170.571 349.281 170.651 349.725 170.651ZM348.907 175.418H351.121C351.449 175.418 351.714 175.356 351.918 175.233C352.121 175.109 352.223 174.895 352.223 174.589C352.223 174.298 352.129 174.091 351.94 173.967C351.758 173.844 351.521 173.782 351.23 173.782H347.761V174.524C347.761 174.822 347.86 175.044 348.056 175.189C348.252 175.342 348.536 175.418 348.907 175.418ZM355.402 176.291L355.293 175.44C356.267 175.171 356.984 174.607 357.442 173.749C357.9 172.884 358.129 171.876 358.129 170.727C358.129 169.578 357.9 168.575 357.442 167.716C356.984 166.851 356.267 166.284 355.293 166.015L355.402 165.164C356.544 165.433 357.464 166.058 358.162 167.04C358.867 168.015 359.22 169.244 359.22 170.727C359.22 172.211 358.867 173.44 358.162 174.415C357.464 175.396 356.544 176.022 355.402 176.291Z" fill="var(--vscode-editor-foreground, #D4D4D4)" /> + <path d="M365.235 174.109C364.872 174.109 364.577 174.029 364.352 173.869C364.126 173.702 364.014 173.44 364.014 173.084V172.778C364.014 172.422 364.126 172.16 364.352 171.993C364.577 171.818 364.872 171.731 365.235 171.731C365.599 171.731 365.89 171.818 366.108 171.993C366.334 172.16 366.446 172.422 366.446 172.778V173.084C366.446 173.44 366.334 173.702 366.108 173.869C365.883 174.029 365.592 174.109 365.235 174.109ZM369.876 174.109C369.512 174.109 369.218 174.029 368.992 173.869C368.767 173.702 368.654 173.44 368.654 173.084V172.778C368.654 172.422 368.767 172.16 368.992 171.993C369.218 171.818 369.512 171.731 369.876 171.731C370.24 171.731 370.531 171.818 370.749 171.993C370.974 172.16 371.087 172.422 371.087 172.778V173.084C371.087 173.44 370.974 173.702 370.749 173.869C370.523 174.029 370.232 174.109 369.876 174.109ZM374.517 174.109C374.153 174.109 373.858 174.029 373.633 173.869C373.408 173.702 373.295 173.44 373.295 173.084V172.778C373.295 172.422 373.408 172.16 373.633 171.993C373.858 171.818 374.153 171.731 374.517 171.731C374.88 171.731 375.171 171.818 375.389 171.993C375.615 172.16 375.728 172.422 375.728 172.778V173.084C375.728 173.44 375.615 173.702 375.389 173.869C375.164 174.029 374.873 174.109 374.517 174.109Z" fill="var(--vscode-editorCodeLens-foreground, #808080)" /> + <rect x="276" y="183" width="226" height="1" fill="var(--vscode-editorWidget-border, #3A3D41)" /> + <path d="M286.091 201V192.273H287.182V195.764H290.455V192.273H291.545V201H290.455V196.745H287.182V201H286.091ZM299.208 199.56C299.012 200.105 298.713 200.502 298.313 200.749C297.913 200.989 297.299 201.109 296.47 201.109C295.604 201.109 294.932 200.902 294.452 200.487C293.972 200.065 293.732 199.444 293.732 198.622V196.865C293.732 196.022 293.982 195.393 294.484 194.978C294.993 194.556 295.655 194.345 296.47 194.345C297.292 194.345 297.95 194.56 298.444 194.989C298.939 195.411 299.186 196.062 299.186 196.942V197.945H294.822V198.633C294.822 199.178 294.957 199.575 295.226 199.822C295.502 200.062 295.913 200.182 296.459 200.182C296.982 200.182 297.364 200.109 297.604 199.964C297.844 199.818 298.026 199.582 298.15 199.255L299.208 199.56ZM294.822 196.865V197.127H298.095V196.964C298.095 196.375 297.957 195.945 297.681 195.676C297.412 195.407 297.004 195.273 296.459 195.273C295.913 195.273 295.502 195.404 295.226 195.665C294.957 195.92 294.822 196.32 294.822 196.865ZM301.372 201V200.018H303.554V193.255H301.372V192.273H304.645V200.018H306.827V201H301.372ZM309.013 201V200.018H311.195V193.255H309.013V192.273H312.286V200.018H314.467V201H309.013ZM319.381 201.109C318.501 201.109 317.824 200.898 317.352 200.476C316.886 200.047 316.653 199.433 316.653 198.633V196.865C316.653 196.058 316.89 195.436 317.363 195C317.835 194.564 318.512 194.345 319.392 194.345C320.264 194.345 320.933 194.564 321.399 195C321.872 195.436 322.108 196.058 322.108 196.865V198.633C322.108 199.433 321.872 200.047 321.399 200.476C320.933 200.898 320.261 201.109 319.381 201.109ZM319.381 200.127C319.933 200.127 320.344 200.004 320.613 199.756C320.883 199.509 321.017 199.135 321.017 198.633V196.865C321.017 196.342 320.883 195.956 320.613 195.709C320.344 195.455 319.933 195.327 319.381 195.327C318.828 195.327 318.417 195.455 318.148 195.709C317.879 195.956 317.744 196.342 317.744 196.865V198.633C317.744 199.135 317.879 199.509 318.148 199.756C318.417 200.004 318.828 200.127 319.381 200.127ZM331.935 201V200.018H334.116V193.255H331.935V192.273H337.389V193.255H335.207V200.018H337.389V201H331.935ZM339.575 201V194.455H340.666V195.545H340.993C341.153 195.175 341.404 194.884 341.746 194.673C342.088 194.455 342.477 194.345 342.913 194.345C343.568 194.345 344.084 194.545 344.463 194.945C344.841 195.345 345.03 195.891 345.03 196.582V201H343.939V196.8C343.939 196.342 343.826 195.993 343.601 195.753C343.383 195.505 343.066 195.382 342.652 195.382C342.324 195.382 341.99 195.48 341.648 195.676C341.313 195.873 340.986 196.156 340.666 196.527V201H339.575ZM351.023 201.098C350.303 201.098 349.743 200.92 349.343 200.564C348.943 200.207 348.743 199.662 348.743 198.927V195.436H346.889V194.455H348.743V192.273H349.834V194.455H352.67V195.436H349.834V198.829C349.834 199.251 349.94 199.564 350.15 199.767C350.361 199.971 350.674 200.073 351.089 200.073C351.365 200.073 351.623 200.055 351.863 200.018C352.103 199.975 352.332 199.92 352.55 199.855L352.769 200.847C352.521 200.92 352.256 200.978 351.972 201.022C351.696 201.073 351.38 201.098 351.023 201.098ZM360.333 199.56C360.137 200.105 359.838 200.502 359.438 200.749C359.038 200.989 358.424 201.109 357.595 201.109C356.729 201.109 356.057 200.902 355.577 200.487C355.097 200.065 354.857 199.444 354.857 198.622V196.865C354.857 196.022 355.107 195.393 355.609 194.978C356.118 194.556 356.78 194.345 357.595 194.345C358.417 194.345 359.075 194.56 359.569 194.989C360.064 195.411 360.311 196.062 360.311 196.942V197.945H355.947V198.633C355.947 199.178 356.082 199.575 356.351 199.822C356.627 200.062 357.038 200.182 357.584 200.182C358.107 200.182 358.489 200.109 358.729 199.964C358.969 199.818 359.151 199.582 359.275 199.255L360.333 199.56ZM355.947 196.865V197.127H359.22V196.964C359.22 196.375 359.082 195.945 358.806 195.676C358.537 195.407 358.129 195.273 357.584 195.273C357.038 195.273 356.627 195.404 356.351 195.665C356.082 195.92 355.947 196.32 355.947 196.865ZM363.043 201V194.455H364.134V195.545H364.461C364.614 195.131 364.846 194.829 365.159 194.64C365.472 194.444 365.85 194.345 366.294 194.345C366.781 194.345 367.188 194.509 367.515 194.836C367.843 195.156 368.006 195.611 368.006 196.2V197.182H366.97V196.593C366.97 196.178 366.875 195.873 366.686 195.676C366.504 195.48 366.224 195.382 365.846 195.382C365.504 195.382 365.192 195.465 364.908 195.633C364.632 195.8 364.374 196.062 364.134 196.418V201H363.043ZM372.08 200.171C372.458 200.171 372.807 200.105 373.127 199.975C373.447 199.836 373.723 199.6 373.956 199.265V198.218H371.992C371.68 198.218 371.418 198.291 371.207 198.436C371.003 198.575 370.901 198.822 370.901 199.178C370.901 199.491 370.989 199.735 371.163 199.909C371.345 200.084 371.651 200.171 372.08 200.171ZM371.938 201.109C371.385 201.109 370.891 200.964 370.454 200.673C370.025 200.375 369.811 199.898 369.811 199.244C369.811 198.669 369.996 198.218 370.367 197.891C370.745 197.564 371.185 197.4 371.687 197.4H373.956V196.462C373.956 196.113 373.84 195.836 373.607 195.633C373.374 195.429 373.021 195.327 372.549 195.327C372.069 195.327 371.709 195.4 371.469 195.545C371.236 195.684 371.051 195.996 370.912 196.484L369.952 196.222C370.076 195.604 370.352 195.138 370.781 194.825C371.218 194.505 371.8 194.345 372.527 194.345C373.247 194.345 373.847 194.509 374.327 194.836C374.807 195.156 375.047 195.655 375.047 196.331V199.255C375.047 199.647 375.156 199.895 375.374 199.996C375.592 200.091 375.92 200.102 376.356 200.029V201.033C375.905 201.098 375.48 201.069 375.08 200.945C374.687 200.815 374.472 200.469 374.436 199.909H374.218C374.014 200.244 373.709 200.527 373.301 200.76C372.894 200.993 372.44 201.109 371.938 201.109ZM380.658 201.109C379.706 201.109 378.986 200.902 378.498 200.487C378.018 200.065 377.778 199.447 377.778 198.633V196.865C377.778 196.036 378.018 195.411 378.498 194.989C378.986 194.56 379.702 194.345 380.648 194.345C381.491 194.345 382.138 194.538 382.589 194.924C383.048 195.302 383.302 195.858 383.353 196.593L382.284 196.658C382.255 196.207 382.106 195.873 381.837 195.655C381.568 195.436 381.168 195.327 380.637 195.327C380.04 195.327 379.597 195.455 379.306 195.709C379.015 195.956 378.869 196.342 378.869 196.865V198.633C378.869 199.135 379.015 199.509 379.306 199.756C379.604 200.004 380.055 200.127 380.658 200.127C381.182 200.127 381.578 200.036 381.848 199.855C382.117 199.665 382.262 199.385 382.284 199.015L383.353 199.08C383.302 199.749 383.051 200.255 382.6 200.596C382.149 200.938 381.502 201.109 380.658 201.109ZM389.226 201.098C388.506 201.098 387.946 200.92 387.546 200.564C387.146 200.207 386.946 199.662 386.946 198.927V195.436H385.092V194.455H386.946V192.273H388.037V194.455H390.874V195.436H388.037V198.829C388.037 199.251 388.143 199.564 388.354 199.767C388.564 199.971 388.877 200.073 389.292 200.073C389.568 200.073 389.826 200.055 390.066 200.018C390.306 199.975 390.535 199.92 390.754 199.855L390.972 200.847C390.724 200.92 390.459 200.978 390.175 201.022C389.899 201.073 389.583 201.098 389.226 201.098ZM393.06 201V200.018H395.241V195.436H393.06V194.455H396.332V200.018H398.514V201H393.06ZM395.787 193.255C395.511 193.255 395.285 193.211 395.111 193.124C394.943 193.036 394.86 192.873 394.86 192.633V192.556C394.86 192.309 394.943 192.142 395.111 192.055C395.285 191.96 395.511 191.913 395.787 191.913C396.063 191.913 396.285 191.96 396.452 192.055C396.627 192.142 396.714 192.309 396.714 192.556V192.633C396.714 192.873 396.627 193.036 396.452 193.124C396.278 193.211 396.056 193.255 395.787 193.255ZM404.104 201H402.74L400.417 194.455H401.606L403.438 200.116L405.271 194.455H406.428L404.104 201ZM413.817 199.56C413.621 200.105 413.323 200.502 412.923 200.749C412.523 200.989 411.908 201.109 411.079 201.109C410.214 201.109 409.541 200.902 409.061 200.487C408.581 200.065 408.341 199.444 408.341 198.622V196.865C408.341 196.022 408.592 195.393 409.094 194.978C409.603 194.556 410.265 194.345 411.079 194.345C411.901 194.345 412.559 194.56 413.054 194.989C413.548 195.411 413.795 196.062 413.795 196.942V197.945H409.432V198.633C409.432 199.178 409.566 199.575 409.835 199.822C410.112 200.062 410.523 200.182 411.068 200.182C411.592 200.182 411.974 200.109 412.214 199.964C412.454 199.818 412.635 199.582 412.759 199.255L413.817 199.56ZM409.432 196.865V197.127H412.705V196.964C412.705 196.375 412.566 195.945 412.29 195.676C412.021 195.407 411.614 195.273 411.068 195.273C410.523 195.273 410.112 195.404 409.835 195.665C409.566 195.92 409.432 196.32 409.432 196.865ZM427.637 201L426.349 195.796L425.062 201H423.971L422.793 192.273H423.873L424.659 199.396L425.804 194.455H426.949L428.095 199.396L428.88 192.273H429.906L428.728 201H427.637ZM431.263 201V200.018H433.445V195.436H431.263V194.455H434.536V200.018H436.717V201H431.263ZM433.99 193.255C433.714 193.255 433.488 193.211 433.314 193.124C433.146 193.036 433.063 192.873 433.063 192.633V192.556C433.063 192.309 433.146 192.142 433.314 192.055C433.488 191.96 433.714 191.913 433.99 191.913C434.266 191.913 434.488 191.96 434.656 192.055C434.83 192.142 434.917 192.309 434.917 192.556V192.633C434.917 192.873 434.83 193.036 434.656 193.124C434.481 193.211 434.259 193.255 433.99 193.255ZM438.903 201V194.455H439.994V195.545H440.322C440.482 195.175 440.733 194.884 441.074 194.673C441.416 194.455 441.805 194.345 442.242 194.345C442.896 194.345 443.413 194.545 443.791 194.945C444.169 195.345 444.358 195.891 444.358 196.582V201H443.267V196.8C443.267 196.342 443.154 195.993 442.929 195.753C442.711 195.505 442.394 195.382 441.98 195.382C441.653 195.382 441.318 195.48 440.976 195.676C440.642 195.873 440.314 196.156 439.994 196.527V201H438.903ZM448.944 200.073C449.3 200.073 449.639 199.978 449.959 199.789C450.279 199.6 450.595 199.313 450.908 198.927V195.72C450.682 195.582 450.442 195.48 450.188 195.415C449.933 195.342 449.66 195.305 449.369 195.305C448.78 195.305 448.344 195.436 448.06 195.698C447.777 195.953 447.635 196.342 447.635 196.865V198.622C447.635 199.109 447.744 199.473 447.962 199.713C448.18 199.953 448.508 200.073 448.944 200.073ZM450.58 199.909C450.391 200.287 450.129 200.582 449.795 200.793C449.46 201.004 449.086 201.109 448.671 201.109C447.995 201.109 447.471 200.891 447.1 200.455C446.729 200.018 446.544 199.407 446.544 198.622V196.844C446.544 196.051 446.791 195.436 447.286 195C447.78 194.564 448.471 194.345 449.359 194.345C449.613 194.345 449.875 194.375 450.144 194.433C450.413 194.484 450.668 194.556 450.908 194.651V192.273H451.999V201H450.908V199.909H450.58ZM456.912 201.109C456.032 201.109 455.356 200.898 454.883 200.476C454.417 200.047 454.185 199.433 454.185 198.633V196.865C454.185 196.058 454.421 195.436 454.894 195C455.366 194.564 456.043 194.345 456.923 194.345C457.796 194.345 458.465 194.564 458.93 195C459.403 195.436 459.639 196.058 459.639 196.865V198.633C459.639 199.433 459.403 200.047 458.93 200.476C458.465 200.898 457.792 201.109 456.912 201.109ZM456.912 200.127C457.465 200.127 457.876 200.004 458.145 199.756C458.414 199.509 458.548 199.135 458.548 198.633V196.865C458.548 196.342 458.414 195.956 458.145 195.709C457.876 195.455 457.465 195.327 456.912 195.327C456.359 195.327 455.948 195.455 455.679 195.709C455.41 195.956 455.276 196.342 455.276 196.865V198.633C455.276 199.135 455.41 199.509 455.679 199.756C455.948 200.004 456.359 200.127 456.912 200.127ZM465.513 201L464.553 196.735L463.636 201H462.393L461.16 194.455H462.207L463.036 200.204L463.887 195.545H465.174L466.102 200.029L466.898 194.455H467.945L466.713 201H465.513ZM471.757 197.727L471.43 192.273H472.957L472.63 197.727H471.757ZM472.204 201.109C471.84 201.109 471.571 201.036 471.397 200.891C471.23 200.738 471.146 200.505 471.146 200.193V199.887C471.146 199.567 471.23 199.331 471.397 199.178C471.571 199.025 471.84 198.949 472.204 198.949C472.568 198.949 472.833 199.025 473 199.178C473.168 199.331 473.251 199.567 473.251 199.887V200.193C473.251 200.505 473.164 200.738 472.99 200.891C472.822 201.036 472.56 201.109 472.204 201.109Z" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <path d="M279.787 306.973L278.987 307.4V319.4L279.787 319.827L288.8 313.853V313L279.787 306.973ZM280 318.493V308.36L287.627 313.427L280 318.493Z" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <rect x="304.5" y="300.5" width="193" height="25" fill="var(--vscode-sideBar-background, #303031)" /> + <rect x="304.5" y="300.5" width="193" height="25" stroke="var(--vscode-editorGroup-border, #424750)" /> + <path d="M436.081 57.2495V65H437.284V62.3521H439.078C440.598 62.3521 441.672 61.3101 441.672 59.8169C441.672 58.2915 440.63 57.2495 439.115 57.2495H436.081ZM437.284 58.2646H438.798C439.856 58.2646 440.442 58.8125 440.442 59.8169C440.442 60.7891 439.835 61.3423 438.798 61.3423H437.284V58.2646ZM443.853 67.1162C445.045 67.1162 445.598 66.6704 446.098 65.2686L448.278 59.167H447.054L445.593 63.8398H445.501L444.035 59.167H442.778L444.895 65.0161L444.809 65.3169C444.61 65.9346 444.298 66.1655 443.75 66.1655C443.643 66.1655 443.466 66.1602 443.375 66.144V67.0894C443.482 67.1055 443.756 67.1162 443.853 67.1162ZM449.868 57.6953V59.1831H448.939V60.1069H449.868V63.4639C449.868 64.5864 450.378 65.0376 451.662 65.0376C451.888 65.0376 452.103 65.0107 452.291 64.9785V64.0601C452.129 64.0762 452.027 64.0869 451.85 64.0869C451.275 64.0869 451.023 63.813 451.023 63.1846V60.1069H452.291V59.1831H451.023V57.6953H449.868ZM453.703 65H454.858V61.584C454.858 60.6709 455.384 60.064 456.33 60.064C457.146 60.064 457.581 60.542 457.581 61.5034V65H458.736V61.2295C458.736 59.8491 457.968 59.0596 456.722 59.0596C455.841 59.0596 455.223 59.4463 454.938 60.0962H454.847V56.8789H453.703V65ZM462.834 65.1128C464.542 65.1128 465.589 63.9688 465.589 62.0835C465.589 60.1982 464.537 59.0542 462.834 59.0542C461.126 59.0542 460.073 60.2036 460.073 62.0835C460.073 63.9688 461.121 65.1128 462.834 65.1128ZM462.834 64.1299C461.83 64.1299 461.266 63.3833 461.266 62.0835C461.266 60.7837 461.83 60.0317 462.834 60.0317C463.833 60.0317 464.402 60.7837 464.402 62.0835C464.402 63.3779 463.833 64.1299 462.834 64.1299ZM466.98 65H468.135V61.5786C468.135 60.6387 468.678 60.0586 469.532 60.0586C470.386 60.0586 470.794 60.5312 470.794 61.498V65H471.949V61.2241C471.949 59.833 471.229 59.0542 469.924 59.0542C469.043 59.0542 468.463 59.4463 468.178 60.0908H468.092V59.167H466.98V65ZM478.297 61.4927H479.264C480.274 61.4927 480.897 62.0083 480.897 62.8354C480.897 63.6411 480.231 64.1782 479.28 64.1782C478.34 64.1782 477.69 63.6895 477.621 62.9268H476.466C476.53 64.291 477.658 65.188 479.296 65.188C480.913 65.188 482.132 64.2212 482.132 62.8999C482.132 61.8364 481.466 61.1489 480.435 60.9932V60.9019C481.246 60.6763 481.832 60.0586 481.837 59.124C481.842 58.0229 480.935 57.0615 479.334 57.0615C477.701 57.0615 476.691 58.0015 476.611 59.3174H477.75C477.819 58.5063 478.383 58.0498 479.264 58.0498C480.134 58.0498 480.65 58.5923 480.65 59.2744C480.65 60.021 480.07 60.5474 479.232 60.5474H478.297V61.4927ZM484.152 65.0806C484.608 65.0806 484.941 64.7422 484.941 64.3071C484.941 63.8721 484.608 63.5337 484.152 63.5337C483.701 63.5337 483.362 63.8721 483.362 64.3071C483.362 64.7422 483.701 65.0806 484.152 65.0806ZM488.889 65.1826C490.828 65.1826 491.961 63.6465 491.961 61.0146C491.961 58.415 490.715 57.0615 488.938 57.0615C487.294 57.0615 486.128 58.1841 486.128 59.7578C486.128 61.251 487.203 62.3413 488.68 62.3413C489.598 62.3413 490.339 61.9116 490.71 61.1704H490.801C490.769 63.0933 490.087 64.1675 488.9 64.1675C488.191 64.1675 487.638 63.7808 487.455 63.1416H486.241C486.472 64.3877 487.514 65.1826 488.889 65.1826ZM488.943 61.3691C487.987 61.3691 487.326 60.6978 487.326 59.7202C487.326 58.7856 488.024 58.0659 488.954 58.0659C489.877 58.0659 490.576 58.7964 490.576 59.7524C490.576 60.6924 489.894 61.3691 488.943 61.3691ZM493.895 65.0806C494.352 65.0806 494.685 64.7422 494.685 64.3071C494.685 63.8721 494.352 63.5337 493.895 63.5337C493.444 63.5337 493.105 63.8721 493.105 64.3071C493.105 64.7422 493.444 65.0806 493.895 65.0806ZM496.752 65H498.015L501.388 58.2915V57.2495H496.108V58.2646H500.168V58.3506L496.752 65Z" fill="var(--vscode-menu-foreground, #CCCCCC)" /> + <rect width="1" height="329" transform="translate(263 10)" fill="var(--vscode-editorGroup-border, #333333)" /> + </g> + <defs> + <filter id="filter0_d_30214690" x="0" y="0" width="526" height="353" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> + <feFlood flood-opacity="0" result="BackgroundImageFix" /> + <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> + <feOffset dy="2" /> + <feGaussianBlur stdDeviation="6" /> + <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0" /> + <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_30214690" /> + <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_30214690" result="shape" /> + </filter> + <clipPath id="clip0_30214690"> + <rect width="114.931" height="39.2998" fill="var(--vscode-list-activeSelectionIconForeground, #FFFFFF)" transform="translate(12 10)" /> + </clipPath> + <clipPath id="clip1_30214690"> + <rect width="143.931" height="39.2998" fill="var(--vscode-list-activeSelectionIconForeground, #FFFFFF)" transform="translate(264 10)" /> + </clipPath> + <clipPath id="clip2_30214690"> + <rect width="143.931" height="39.2998" fill="var(--vscode-list-activeSelectionIconForeground, #FFFFFF)" transform="translate(264 10)" /> + </clipPath> + </defs> +</svg> \ 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 @@ +<svg width="537" height="324" viewBox="0 0 537 324" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="386" y="261" width="77" height="59" rx="2" stroke="var(--vscode-dropdown-border, #3c3c3c)" stroke-width="2" stroke-linecap="round" stroke-dasharray="5 5"/> +<rect x="1" y="1" width="379" height="267" rx="2" stroke="var(--vscode-dropdown-border, #3c3c3c)" stroke-width="2" stroke-linecap="round" stroke-dasharray="5 5"/> +<g filter="url(#filter0_d)"> +<g clip-path="url(#clip0)"> +<rect x="21" y="23" width="431" height="287" rx="4" fill="var(--vscode-editor-background, #1e1e1e)"/> +<path d="M21 27C21 24.7909 22.7909 23 25 23H448C450.209 23 452 24.7909 452 27V43H21V27Z" fill="var(--vscode-titleBar-activeBackground, #303031)"/> +<rect width="72" height="15" transform="translate(63 28)" fill="var(--vscode-tab-activeBackground, #1e1e1e)"/> +<rect x="71.4062" y="33" width="55.3408" height="4.90362" rx="2.45181" fill="var(--vscode-foreground)" fill-opacity="0.25"/> +<circle cx="50.4108" cy="33.4265" r="2.42647" fill="#40C8AE"/> +<circle cx="41.9206" cy="33.4265" r="2.42647" fill="#D7BA7D"/> +<circle cx="33.4265" cy="33.427" r="2.42647" fill="#F14C4C"/> +<rect x="48" y="217" width="303" height="8" rx="4" fill="var(--vscode-tab-activeForeground)" fill-opacity="0.16"/> +<rect x="48" y="232" width="212.281" height="8" rx="4" fill="var(--vscode-tab-activeForeground)" fill-opacity="0.16"/> +<rect x="267.281" y="232" width="58.0599" height="8" rx="4" fill="var(--vscode-textLink-foreground, #007ACC)"/> +<rect x="48" y="272" width="99.7904" height="8" rx="4" fill="var(--vscode-tab-activeForeground)" fill-opacity="0.16"/> +<rect x="48" y="287" width="50" height="8" rx="4" fill="var(--vscode-tab-activeForeground)" fill-opacity="0.16"/> +<rect x="170" y="287" width="181" height="8" rx="4" fill="var(--vscode-tab-activeForeground)" fill-opacity="0.16"/> +<rect x="105" y="287" width="58.0599" height="8" rx="4" fill="var(--vscode-textLink-foreground, #007ACC)"/> +<rect x="48" y="132" width="160" height="8" rx="4" fill="var(--vscode-tab-activeForeground)" fill-opacity="0.16"/> +<rect x="280" y="132" width="71" height="8" rx="4" fill="var(--vscode-tab-activeForeground)" fill-opacity="0.16"/> +<rect x="215" y="132" width="58.0599" height="8" rx="4" fill="var(--vscode-textLink-foreground, #007ACC)"/> +<rect x="48" y="162" width="281.228" height="8" rx="4" fill="var(--vscode-tab-activeForeground)" fill-opacity="0.16"/> +<rect x="48" y="177" width="100" height="8" rx="4" fill="#6C6CC4"/> +<rect x="48" y="147" width="303" height="8" rx="4" fill="var(--vscode-tab-activeForeground)" fill-opacity="0.16"/> +<g filter="url(#filter1_d)"> +<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="49" y="60" width="14" height="14"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M58.9277 73.9044C59.1482 73.9903 59.3996 73.9848 59.6221 73.8777L62.5045 72.4908C62.8074 72.3451 63 72.0385 63 71.7022V62.2979C63 61.9616 62.8074 61.6551 62.5045 61.5093L59.6221 60.1224C59.3301 59.9818 58.9882 60.0162 58.7319 60.2026C58.6953 60.2292 58.6604 60.259 58.6277 60.2917L53.1097 65.3258L50.7062 63.5014C50.4825 63.3315 50.1695 63.3455 49.9617 63.5345L49.1908 64.2357C48.9366 64.4669 48.9364 64.8668 49.1902 65.0984L51.2746 67L49.1902 68.9017C48.9364 69.1333 48.9366 69.5332 49.1908 69.7644L49.9617 70.4656C50.1695 70.6546 50.4825 70.6685 50.7062 70.4987L53.1097 68.6743L58.6277 73.7084C58.7149 73.7957 58.8174 73.8615 58.9277 73.9044ZM59.5021 63.8219L55.3153 67L59.5021 70.1782V63.8219Z" fill="white"/> +</mask> +<g mask="url(#mask0)"> +<path d="M62.5053 61.5114L59.6207 60.1225C59.2868 59.9617 58.8877 60.0295 58.6257 60.2916L49.1825 68.9016C48.9285 69.1331 48.9288 69.533 49.1831 69.7642L49.9545 70.4655C50.1624 70.6545 50.4756 70.6684 50.6994 70.4986L62.0712 61.8717C62.4527 61.5823 63.0007 61.8544 63.0007 62.3332V62.2998C63.0007 61.9636 62.8081 61.6572 62.5053 61.5114Z" fill="#0065A9"/> +<g filter="url(#filter2_d)"> +<path d="M62.5053 72.4886L59.6207 73.8775C59.2868 74.0383 58.8877 73.9705 58.6257 73.7084L49.1825 65.0984C48.9285 64.8668 48.9288 64.467 49.1831 64.2357L49.9545 63.5345C50.1624 63.3455 50.4756 63.3316 50.6994 63.5014L62.0712 72.1283C62.4527 72.4177 63.0007 72.1456 63.0007 71.6667V71.7002C63.0007 72.0364 62.8081 72.3428 62.5053 72.4886Z" fill="#007ACC"/> +</g> +<g filter="url(#filter3_d)"> +<path d="M59.6201 73.8777C59.2861 74.0384 58.8871 73.9704 58.625 73.7084C58.9479 74.0313 59.5 73.8026 59.5 73.3459V60.6541C59.5 60.1975 58.9479 59.9688 58.625 60.2917C58.8871 60.0296 59.2861 59.9617 59.6201 60.1224L62.5042 61.5093C62.8073 61.6551 63 61.9616 63 62.2979V71.7022C63 72.0385 62.8073 72.3451 62.5042 72.4908L59.6201 73.8777Z" fill="#1F9CF0"/> +</g> +<g style="mix-blend-mode:overlay" opacity="0.25"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M58.9199 73.9044C59.1403 73.9903 59.3918 73.9848 59.6143 73.8777L62.4967 72.4908C62.7996 72.3451 62.9922 72.0385 62.9922 71.7022V62.2979C62.9922 61.9616 62.7996 61.6551 62.4967 61.5093L59.6143 60.1224C59.3223 59.9818 58.9804 60.0162 58.7241 60.2026C58.6875 60.2292 58.6526 60.259 58.6198 60.2917L53.1019 65.3258L50.6984 63.5014C50.4747 63.3315 50.1617 63.3455 49.9539 63.5345L49.183 64.2357C48.9288 64.4669 48.9285 64.8668 49.1824 65.0984L51.2668 67L49.1824 68.9017C48.9285 69.1333 48.9288 69.5332 49.183 69.7644L49.9539 70.4656C50.1617 70.6546 50.4747 70.6685 50.6984 70.4987L53.1019 68.6742L58.6198 73.7084C58.7071 73.7957 58.8096 73.8615 58.9199 73.9044ZM59.4943 63.8219L55.3075 67L59.4943 70.1782V63.8219Z" fill="url(#paint0_linear)"/> +</g> +</g> +</g> +<path d="M50.2598 87.6807V106H53.5352V99.9443H57.5088C61.2158 99.9443 63.8057 97.4688 63.8057 93.8506C63.8057 90.1689 61.3047 87.6807 57.6484 87.6807H50.2598ZM53.5352 90.3594H56.7852C59.1465 90.3594 60.4795 91.5781 60.4795 93.8506C60.4795 96.0596 59.1084 97.3037 56.7725 97.3037H53.5352V90.3594ZM68.9727 111.027C72.083 111.027 73.5684 109.897 74.749 106.482L79.7637 92.124H76.4375L73.2002 103.017H72.9844L69.7344 92.124H66.2812L71.2451 106.051L71.0801 106.698C70.6738 108.006 69.9248 108.514 68.5918 108.514C68.376 108.514 67.9062 108.501 67.7285 108.476V110.977C67.9316 111.015 68.7822 111.027 68.9727 111.027ZM83.4834 88.7217V92.2129H81.2871V94.6631H83.4834V102.318C83.4834 104.997 84.7529 106.063 87.9395 106.063C88.5488 106.063 89.1328 106.013 89.5898 105.924V103.512C89.209 103.55 88.9678 103.575 88.5234 103.575C87.2031 103.575 86.6191 102.953 86.6191 101.569V94.6631H89.5898V92.2129H86.6191V88.7217H83.4834ZM92.8652 106H96.0137V97.9385C96.0137 95.9072 97.1689 94.5488 99.2383 94.5488C101.066 94.5488 102.031 95.6406 102.031 97.7861V106H105.18V97.0498C105.18 93.749 103.377 91.8701 100.406 91.8701C98.3369 91.8701 96.8516 92.8096 96.1914 94.3711H95.9629V86.7539H92.8652V106ZM114.904 106.279C119.056 106.279 121.582 103.562 121.582 99.0557C121.582 94.5742 119.043 91.8574 114.904 91.8574C110.778 91.8574 108.227 94.5869 108.227 99.0557C108.227 103.562 110.74 106.279 114.904 106.279ZM114.904 103.702C112.708 103.702 111.451 102.001 111.451 99.0684C111.451 96.1357 112.708 94.4346 114.904 94.4346C117.088 94.4346 118.345 96.1357 118.345 99.0684C118.345 102.001 117.101 103.702 114.904 103.702ZM124.756 106H127.904V97.9131C127.904 95.8311 129.085 94.5234 130.964 94.5234C132.881 94.5234 133.77 95.6025 133.77 97.7607V106H136.918V97.0244C136.918 93.7236 135.229 91.8574 132.132 91.8574C130.062 91.8574 128.666 92.7969 128.006 94.3457H127.79V92.124H124.756V106ZM149.182 89.8008C150.223 89.8008 151.073 88.9756 151.073 87.9346C151.073 86.9062 150.223 86.0684 149.182 86.0684C148.153 86.0684 147.303 86.9062 147.303 87.9346C147.303 88.9756 148.153 89.8008 149.182 89.8008ZM147.62 106H150.756V92.124H147.62V106ZM154.666 106H157.814V97.9131C157.814 95.8311 158.995 94.5234 160.874 94.5234C162.791 94.5234 163.68 95.6025 163.68 97.7607V106H166.828V97.0244C166.828 93.7236 165.14 91.8574 162.042 91.8574C159.973 91.8574 158.576 92.7969 157.916 94.3457H157.7V92.124H154.666V106ZM186.633 106L192.968 87.6807H189.477L184.906 102.255H184.69L180.082 87.6807H176.464L182.862 106H186.633ZM195.113 101.036C195.278 104.35 198.059 106.457 202.261 106.457C206.704 106.457 209.459 104.248 209.459 100.693C209.459 97.9131 207.897 96.3643 204.178 95.5137L202.07 95.0312C199.798 94.498 198.871 93.7744 198.871 92.5176C198.871 90.9307 200.268 89.8896 202.375 89.8896C204.381 89.8896 205.803 90.918 206.031 92.5557H209.167C209.015 89.4199 206.234 87.2236 202.388 87.2236C198.287 87.2236 195.57 89.4326 195.57 92.7334C195.57 95.4502 197.132 97.0879 200.458 97.8496L202.832 98.3955C205.181 98.9414 206.184 99.7539 206.184 101.087C206.184 102.648 204.622 103.778 202.464 103.778C200.141 103.778 198.516 102.712 198.312 101.036H195.113ZM227.715 106.457C231.942 106.457 235.04 103.956 235.472 100.249H232.247C231.803 102.306 230.038 103.613 227.715 103.613C224.592 103.613 222.649 101.011 222.649 96.834C222.649 92.6699 224.592 90.0674 227.702 90.0674C230.013 90.0674 231.777 91.502 232.234 93.7109H235.459C235.078 89.915 231.866 87.2236 227.702 87.2236C222.51 87.2236 219.298 90.8926 219.298 96.8467C219.298 102.775 222.522 106.457 227.715 106.457ZM244.777 106.279C248.929 106.279 251.455 103.562 251.455 99.0557C251.455 94.5742 248.916 91.8574 244.777 91.8574C240.651 91.8574 238.1 94.5869 238.1 99.0557C238.1 103.562 240.613 106.279 244.777 106.279ZM244.777 103.702C242.581 103.702 241.324 102.001 241.324 99.0684C241.324 96.1357 242.581 94.4346 244.777 94.4346C246.961 94.4346 248.218 96.1357 248.218 99.0684C248.218 102.001 246.974 103.702 244.777 103.702ZM259.656 106.229C261.586 106.229 263.186 105.327 263.973 103.804H264.188V106H267.235V86.7539H264.087V94.333H263.871C263.135 92.8096 261.561 91.8955 259.656 91.8955C256.152 91.8955 253.943 94.6631 253.943 99.0557C253.943 103.461 256.14 106.229 259.656 106.229ZM260.634 94.5234C262.792 94.5234 264.125 96.2627 264.125 99.0684C264.125 101.887 262.805 103.613 260.634 103.613C258.476 103.613 257.168 101.912 257.168 99.0684C257.168 96.2373 258.488 94.5234 260.634 94.5234ZM280.261 102.204C279.804 103.245 278.725 103.829 277.163 103.829C275.094 103.829 273.761 102.356 273.685 100.008V99.8428H283.384V98.8398C283.384 94.4854 281.01 91.8574 277.049 91.8574C273.024 91.8574 270.523 94.6631 270.523 99.1191C270.523 103.562 272.986 106.279 277.074 106.279C280.35 106.279 282.673 104.705 283.244 102.204H280.261ZM277.036 94.3076C278.928 94.3076 280.172 95.6406 280.235 97.748H273.697C273.837 95.666 275.157 94.3076 277.036 94.3076Z" fill="var(--vscode-tab-activeForeground)"/> +</g> +</g> +<rect x="422" y="235" width="65" height="43" fill="var(--vscode-editor-background, #1e1e1e)"/> +<path d="M419.067 232.79L421.467 230.123H486.533L488.933 232.79V277.59L486.533 280.256H421.467L419.067 277.59V232.79ZM424.133 237.856V275.19H484.133V237.856L455.6 259.723H452.4L424.133 237.856ZM479.067 235.19H428.933L454 254.656L479.067 235.19Z" fill="var(--vscode-foreground, #CCCCCC)"/> +<path d="M529.238 159.867H464.172L461.772 162.533V207.6L464.172 210H476.705V222.533L480.972 224.4L495.372 210H529.238L531.638 207.6V162.533L529.238 159.867ZM526.838 204.933H494.305L492.438 205.733L481.772 216.4V207.6L479.105 204.933H466.838V164.933H526.838V204.933Z" fill="var(--vscode-foreground, #CCCCCC)"/> +<defs> +<filter id="filter0_d" x="9" y="13" width="455" height="311" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="2"/> +<feGaussianBlur stdDeviation="6"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/> +</filter> +<filter id="filter1_d" x="47.1284" y="58.7523" width="17.7432" height="17.7432" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="0.623874"/> +<feGaussianBlur stdDeviation="0.935811"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/> +</filter> +<filter id="filter2_d" x="44.6251" y="59.0157" width="22.7427" height="19.3153" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset/> +<feGaussianBlur stdDeviation="2.18356"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/> +<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/> +</filter> +<filter id="filter3_d" x="54.2579" y="55.669" width="13.1092" height="22.6621" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset/> +<feGaussianBlur stdDeviation="2.18356"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/> +<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/> +</filter> +<linearGradient id="paint0_linear" x1="55.9922" y1="60.0361" x2="55.9922" y2="73.964" gradientUnits="userSpaceOnUse"> +<stop stop-color="white"/> +<stop offset="1" stop-color="white" stop-opacity="0"/> +</linearGradient> +<clipPath id="clip0"> +<rect x="21" y="23" width="431" height="287" rx="4" fill="white"/> +</clipPath> +</defs> +</svg> 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 @@ +<svg width="410" height="370" viewBox="0 0 410 370" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g clip-path="url(#clip0_30514464)"> + <rect width="198" height="318" transform="translate(212 24)" fill="var(--vscode-editor-background, #1E1E1E)" /> + <rect width="336" height="35" transform="translate(212 24)" fill="var(--vscode-editorGroupHeader-tabsBackground, #252526)" /> + <rect width="103" height="35" transform="translate(212 24)" fill="var(--vscode-editor-background, #1E1E1E)" /> + <rect x="224" y="38" width="79" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.12" /> + <rect x="255" y="77" width="55" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect x="255" y="96" width="55" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect x="255" y="153" width="72" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect x="333" y="153" width="26" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect x="255" y="273" width="72" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect x="333" y="273" width="26" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect x="272" y="172" width="30" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect x="282" y="191" width="30" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect x="318" y="191" width="41" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect x="272" y="210" width="30" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect x="263" y="229" width="31" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect x="316" y="77" width="52" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect x="316" y="96" width="74" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect x="309" y="172" width="59" height="7" rx="3.5" fill="var(--vscode-editor-foreground, #FFFFFF)" fill-opacity="0.16" /> + </g> + <g filter="url(#filter0_d_30514464)"> + <rect x="13" y="11" width="212.268" height="344" fill="var(--vscode-sideBar-background, #292929)" /> + <path d="M34.8806 39.7488H31.8019V37.7511H34.716V36.9236H31.8019V35.0461H34.8806V34.1875H30.8053V40.6074H34.8806V39.7488ZM35.9128 40.6074H37.0161L38.6133 38.1916H38.689L40.2639 40.6074H41.4296L39.2896 37.4086L41.4696 34.1875H40.3485L38.7602 36.6389H38.6845L37.1229 34.1875H35.935L38.0572 37.3774L35.9128 40.6074ZM42.6753 34.1875V40.6074H43.6719V38.414H45.1579C46.4169 38.414 47.3067 37.5509 47.3067 36.3141C47.3067 35.0506 46.4436 34.1875 45.189 34.1875H42.6753ZM43.6719 35.0283H44.9265C45.803 35.0283 46.2879 35.4821 46.2879 36.3141C46.2879 37.1194 45.7852 37.5776 44.9265 37.5776H43.6719V35.0283ZM52.5967 39.7399H49.5891V34.1875H48.5925V40.6074H52.5967V39.7399ZM56.3561 34.0317C54.5142 34.0317 53.3574 35.3264 53.3574 37.3952C53.3574 39.4596 54.4875 40.7631 56.3561 40.7631C58.2113 40.7631 59.3503 39.4551 59.3503 37.3952C59.3503 35.3309 58.2024 34.0317 56.3561 34.0317ZM56.3561 34.9127C57.5707 34.9127 58.3315 35.8736 58.3315 37.3952C58.3315 38.9034 57.5751 39.8822 56.3561 39.8822C55.1193 39.8822 54.3763 38.9034 54.3763 37.3952C54.3763 35.8736 55.1415 34.9127 56.3561 34.9127ZM61.6994 38.1382H63.1053L64.4133 40.6074H65.5656L64.1241 37.9825C64.9071 37.72 65.3698 37.017 65.3698 36.1495C65.3698 34.9527 64.5379 34.1875 63.2432 34.1875H60.7028V40.6074H61.6994V38.1382ZM61.6994 35.0239H63.1053C63.875 35.0239 64.3466 35.4554 64.3466 36.1717C64.3466 36.9058 63.9017 37.3196 63.132 37.3196H61.6994V35.0239ZM70.8644 39.7488H67.7857V37.7511H70.6998V36.9236H67.7857V35.0461H70.8644V34.1875H66.7891V40.6074H70.8644V39.7488ZM73.3025 38.1382H74.7084L76.0164 40.6074H77.1687L75.7272 37.9825C76.5102 37.72 76.9729 37.017 76.9729 36.1495C76.9729 34.9527 76.141 34.1875 74.8463 34.1875H72.3059V40.6074H73.3025V38.1382ZM73.3025 35.0239H74.7084C75.478 35.0239 75.9496 35.4554 75.9496 36.1717C75.9496 36.9058 75.5047 37.3196 74.7351 37.3196H73.3025V35.0239ZM79.0239 37.3329C79.4021 37.3329 79.6779 37.0526 79.6779 36.6923C79.6779 36.3319 79.4021 36.0516 79.0239 36.0516C78.6502 36.0516 78.3699 36.3319 78.3699 36.6923C78.3699 37.0526 78.6502 37.3329 79.0239 37.3329ZM79.0239 40.6742C79.4021 40.6742 79.6779 40.3939 79.6779 40.0335C79.6779 39.6731 79.4021 39.3928 79.0239 39.3928C78.6502 39.3928 78.3699 39.6731 78.3699 40.0335C78.3699 40.3939 78.6502 40.6742 79.0239 40.6742ZM90.2533 40.6074V34.1875H89.0876L87.0411 39.2104H86.9655L84.9233 34.1875H83.7577V40.6074H84.6831V35.9048H84.7454L86.6273 40.4917H87.3837L89.2656 35.9048H89.3279V40.6074H90.2533ZM94.6311 40.6074V37.978L96.8957 34.1875H95.7879L94.1729 36.9859H94.0973L92.4823 34.1875H91.3745L93.6346 37.978V40.6074H94.6311ZM100.179 38.463V37.5776H97.2605V38.463H100.179ZM105.674 40.6074H106.733L104.415 34.1875H103.342L101.024 40.6074H102.048L102.639 38.8723H105.086L105.674 40.6074ZM103.827 35.322H103.903L104.833 38.0715H102.893L103.827 35.322ZM107.854 34.1875V40.6074H108.85V38.414H110.336C111.595 38.414 112.485 37.5509 112.485 36.3141C112.485 35.0506 111.622 34.1875 110.367 34.1875H107.854ZM108.85 35.0283H110.105C110.981 35.0283 111.466 35.4821 111.466 36.3141C111.466 37.1194 110.964 37.5776 110.105 37.5776H108.85V35.0283ZM113.771 34.1875V40.6074H114.767V38.414H116.253C117.513 38.414 118.402 37.5509 118.402 36.3141C118.402 35.0506 117.539 34.1875 116.285 34.1875H113.771ZM114.767 35.0283H116.022C116.899 35.0283 117.384 35.4821 117.384 36.3141C117.384 37.1194 116.881 37.5776 116.022 37.5776H114.767V35.0283Z" fill="var(--vscode-foreground, #CCCCCC)" /> + <path d="M35.984 65.1001L40.336 60.7481L40.96 61.3721L36.288 66.0281H35.664L31.008 61.3721L31.616 60.7481L35.984 65.1001Z" fill="var(--vscode-foreground, #808080)" /> + <rect x="52" y="59.5361" width="42" height="7" rx="3.5" fill="var(--vscode-foreground, #FFFFFF)" fill-opacity="0.16" /> + <path d="M47.984 89.1001L52.336 84.7481L52.96 85.3721L48.288 90.0281H47.664L43.008 85.3721L43.616 84.7481L47.984 89.1001Z" fill="var(--vscode-foreground, #808080)" /> + <rect x="64" y="83.5361" width="42" height="7" rx="3.5" fill="var(--vscode-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect width="210.268" height="24" transform="translate(14 99.0361)" fill="var(--vscode-list-activeSelectionBackground, #062F4A)" /> + <path d="M47.76 110.716H46.5C46.06 110.716 45.7067 110.849 45.44 111.116C45.1867 111.369 45.06 111.716 45.06 112.156V113.276C45.06 113.409 45 113.476 44.88 113.476H44.32C43.7333 113.476 43.3133 113.223 43.06 112.716C42.86 112.303 42.76 111.929 42.76 111.596C42.68 110.769 42.74 110.083 42.94 109.536C43.1533 108.896 43.5467 108.523 44.12 108.416H47.74C47.8733 108.416 47.94 108.396 47.94 108.356V108.036L47.86 107.996C47.8067 107.983 47.7733 107.976 47.76 107.976H45.62C45.5267 107.976 45.46 107.956 45.42 107.916C45.3933 107.876 45.38 107.809 45.38 107.716V106.916C45.38 106.449 45.5667 106.156 45.94 106.036C46.3533 105.863 46.6667 105.756 46.88 105.716C47.68 105.583 48.4267 105.603 49.12 105.776C49.4667 105.856 49.76 105.983 50 106.156C50.1467 106.303 50.2467 106.436 50.3 106.556C50.38 106.703 50.4067 106.863 50.38 107.036V109.276C50.38 109.716 50.26 110.056 50.02 110.296C49.78 110.536 49.44 110.656 49 110.656C48.7067 110.696 48.2933 110.716 47.76 110.716ZM46 106.976C46 107.109 46.0467 107.229 46.14 107.336C46.2333 107.429 46.3467 107.476 46.48 107.476C46.6133 107.476 46.7333 107.423 46.84 107.316C46.9467 107.209 47 107.096 47 106.976C47 106.856 46.9467 106.749 46.84 106.656C46.7467 106.563 46.6333 106.503 46.5 106.476C46.3533 106.476 46.2333 106.529 46.14 106.636C46.0467 106.729 46 106.843 46 106.976ZM48.26 111.356H49.5C49.94 111.356 50.2867 111.229 50.54 110.976C50.8067 110.709 50.94 110.356 50.94 109.916V108.776C50.94 108.656 51 108.596 51.12 108.596H51.68C52.2667 108.596 52.6867 108.849 52.94 109.356C53.1533 109.769 53.26 110.143 53.26 110.476C53.3267 111.303 53.26 111.989 53.06 112.536C52.8467 113.176 52.4533 113.549 51.88 113.656H48.26C48.1267 113.656 48.06 113.676 48.06 113.716V114.036L48.14 114.076C48.1933 114.089 48.2333 114.096 48.26 114.096H50.38C50.4733 114.096 50.5333 114.116 50.56 114.156C50.6 114.196 50.62 114.263 50.62 114.356V115.156C50.62 115.623 50.4333 115.916 50.06 116.036C49.6467 116.196 49.3333 116.303 49.12 116.356C48.32 116.489 47.5733 116.463 46.88 116.276C46.5333 116.209 46.24 116.089 46 115.916C45.8533 115.769 45.7533 115.636 45.7 115.516C45.62 115.369 45.5933 115.209 45.62 115.036V112.776C45.62 112.349 45.74 112.016 45.98 111.776C46.22 111.536 46.56 111.416 47 111.416C47.2933 111.376 47.7133 111.356 48.26 111.356ZM50 115.096C50 114.963 49.9533 114.849 49.86 114.756C49.7667 114.649 49.6533 114.596 49.52 114.596C49.3867 114.596 49.2667 114.649 49.16 114.756C49.0533 114.863 49 114.976 49 115.096C49 115.216 49.0467 115.323 49.14 115.416C49.2467 115.509 49.3667 115.569 49.5 115.596C49.6467 115.596 49.7667 115.549 49.86 115.456C49.9533 115.349 50 115.229 50 115.096Z" fill="#519ABA" /> + <path d="M66.9009 115.15C67.8086 115.15 68.564 114.757 68.9766 114.059H69.0845V115.036H70.3984V110.32C70.3984 108.873 69.4209 108.009 67.688 108.009C66.1201 108.009 65.0029 108.765 64.8633 109.926H66.1836C66.3359 109.425 66.8628 109.139 67.6245 109.139C68.5576 109.139 69.04 109.564 69.04 110.32V110.923L67.1675 111.037C65.5234 111.139 64.5967 111.856 64.5967 113.094C64.5967 114.351 65.5679 115.15 66.9009 115.15ZM67.25 114.052C66.5073 114.052 65.9678 113.678 65.9678 113.037C65.9678 112.408 66.3994 112.072 67.3516 112.008L69.04 111.894V112.491C69.04 113.379 68.2783 114.052 67.25 114.052ZM75.9272 108.022C74.9878 108.022 74.1753 108.498 73.7563 109.285H73.6548V108.143H72.3408V117.334H73.7056V113.995H73.8135C74.1753 114.725 74.9561 115.15 75.9399 115.15C77.6855 115.15 78.7964 113.767 78.7964 111.589C78.7964 109.399 77.6855 108.022 75.9272 108.022ZM75.54 113.982C74.3975 113.982 73.6802 113.062 73.6802 111.589C73.6802 110.11 74.3975 109.19 75.5464 109.19C76.7017 109.19 77.3936 110.091 77.3936 111.589C77.3936 113.087 76.7017 113.982 75.54 113.982ZM84.0269 108.022C83.0874 108.022 82.2749 108.498 81.856 109.285H81.7544V108.143H80.4404V117.334H81.8052V113.995H81.9131C82.2749 114.725 83.0557 115.15 84.0396 115.15C85.7852 115.15 86.896 113.767 86.896 111.589C86.896 109.399 85.7852 108.022 84.0269 108.022ZM83.6396 113.982C82.4971 113.982 81.7798 113.062 81.7798 111.589C81.7798 110.11 82.4971 109.19 83.646 109.19C84.8013 109.19 85.4932 110.091 85.4932 111.589C85.4932 113.087 84.8013 113.982 83.6396 113.982ZM89.4478 115.131C89.9873 115.131 90.3809 114.731 90.3809 114.217C90.3809 113.703 89.9873 113.303 89.4478 113.303C88.9146 113.303 88.5146 113.703 88.5146 114.217C88.5146 114.731 88.9146 115.131 89.4478 115.131ZM96.0747 108.022C95.1353 108.022 94.3228 108.498 93.9038 109.285H93.8022V108.143H92.4883V117.334H93.853V113.995H93.9609C94.3228 114.725 95.1035 115.15 96.0874 115.15C97.833 115.15 98.9438 113.767 98.9438 111.589C98.9438 109.399 97.833 108.022 96.0747 108.022ZM95.6875 113.982C94.5449 113.982 93.8276 113.062 93.8276 111.589C93.8276 110.11 94.5449 109.19 95.6938 109.19C96.8491 109.19 97.541 110.091 97.541 111.589C97.541 113.087 96.8491 113.982 95.6875 113.982ZM101.007 117.537C102.416 117.537 103.07 117.01 103.66 115.354L106.237 108.143H104.79L103.063 113.665H102.956L101.223 108.143H99.7373L102.238 115.055L102.137 115.411C101.902 116.141 101.534 116.414 100.886 116.414C100.759 116.414 100.55 116.407 100.442 116.388V117.505C100.569 117.524 100.893 117.537 101.007 117.537Z" fill="var(--vscode-list-activeSelectionForeground, #FFFFFF)" /> + <path d="M47.76 134.716H46.5C46.06 134.716 45.7067 134.849 45.44 135.116C45.1867 135.369 45.06 135.716 45.06 136.156V137.276C45.06 137.409 45 137.476 44.88 137.476H44.32C43.7333 137.476 43.3133 137.223 43.06 136.716C42.86 136.303 42.76 135.929 42.76 135.596C42.68 134.769 42.74 134.083 42.94 133.536C43.1533 132.896 43.5467 132.523 44.12 132.416H47.74C47.8733 132.416 47.94 132.396 47.94 132.356V132.036L47.86 131.996C47.8067 131.983 47.7733 131.976 47.76 131.976H45.62C45.5267 131.976 45.46 131.956 45.42 131.916C45.3933 131.876 45.38 131.809 45.38 131.716V130.916C45.38 130.449 45.5667 130.156 45.94 130.036C46.3533 129.863 46.6667 129.756 46.88 129.716C47.68 129.583 48.4267 129.603 49.12 129.776C49.4667 129.856 49.76 129.983 50 130.156C50.1467 130.303 50.2467 130.436 50.3 130.556C50.38 130.703 50.4067 130.863 50.38 131.036V133.276C50.38 133.716 50.26 134.056 50.02 134.296C49.78 134.536 49.44 134.656 49 134.656C48.7067 134.696 48.2933 134.716 47.76 134.716ZM46 130.976C46 131.109 46.0467 131.229 46.14 131.336C46.2333 131.429 46.3467 131.476 46.48 131.476C46.6133 131.476 46.7333 131.423 46.84 131.316C46.9467 131.209 47 131.096 47 130.976C47 130.856 46.9467 130.749 46.84 130.656C46.7467 130.563 46.6333 130.503 46.5 130.476C46.3533 130.476 46.2333 130.529 46.14 130.636C46.0467 130.729 46 130.843 46 130.976ZM48.26 135.356H49.5C49.94 135.356 50.2867 135.229 50.54 134.976C50.8067 134.709 50.94 134.356 50.94 133.916V132.776C50.94 132.656 51 132.596 51.12 132.596H51.68C52.2667 132.596 52.6867 132.849 52.94 133.356C53.1533 133.769 53.26 134.143 53.26 134.476C53.3267 135.303 53.26 135.989 53.06 136.536C52.8467 137.176 52.4533 137.549 51.88 137.656H48.26C48.1267 137.656 48.06 137.676 48.06 137.716V138.036L48.14 138.076C48.1933 138.089 48.2333 138.096 48.26 138.096H50.38C50.4733 138.096 50.5333 138.116 50.56 138.156C50.6 138.196 50.62 138.263 50.62 138.356V139.156C50.62 139.623 50.4333 139.916 50.06 140.036C49.6467 140.196 49.3333 140.303 49.12 140.356C48.32 140.489 47.5733 140.463 46.88 140.276C46.5333 140.209 46.24 140.089 46 139.916C45.8533 139.769 45.7533 139.636 45.7 139.516C45.62 139.369 45.5933 139.209 45.62 139.036V136.776C45.62 136.349 45.74 136.016 45.98 135.776C46.22 135.536 46.56 135.416 47 135.416C47.2933 135.376 47.7133 135.356 48.26 135.356ZM50 139.096C50 138.963 49.9533 138.849 49.86 138.756C49.7667 138.649 49.6533 138.596 49.52 138.596C49.3867 138.596 49.2667 138.649 49.16 138.756C49.0533 138.863 49 138.976 49 139.096C49 139.216 49.0467 139.323 49.14 139.416C49.2467 139.509 49.3667 139.569 49.5 139.596C49.6467 139.596 49.7667 139.549 49.86 139.456C49.9533 139.349 50 139.229 50 139.096Z" fill="#519ABA" /> + <path d="M65.4727 130.403V132.162H64.3745V133.253H65.4727V137.221C65.4727 138.547 66.0757 139.081 67.5928 139.081C67.8594 139.081 68.1133 139.049 68.3354 139.011V137.925C68.145 137.944 68.0244 137.957 67.8149 137.957C67.1357 137.957 66.8374 137.633 66.8374 136.891V133.253H68.3354V132.162H66.8374V130.403H65.4727ZM74.3975 137.189C74.1436 137.748 73.5786 138.052 72.7534 138.052C71.6616 138.052 70.957 137.265 70.9126 136.015V135.951H75.7939V135.481C75.7939 133.323 74.6323 132.009 72.709 132.009C70.7603 132.009 69.5225 133.412 69.5225 135.602C69.5225 137.805 70.7349 139.169 72.7153 139.169C74.2959 139.169 75.4131 138.408 75.7114 137.189H74.3975ZM72.7026 133.12C73.7119 133.12 74.3721 133.85 74.4038 134.967H70.9126C70.9888 133.856 71.6934 133.12 72.7026 133.12ZM77.2729 134.072C77.2729 135.094 77.8823 135.678 79.1963 135.983L80.4023 136.269C81.0942 136.434 81.418 136.713 81.418 137.145C81.418 137.709 80.8149 138.103 79.9644 138.103C79.1392 138.103 78.6313 137.767 78.46 137.227H77.0952C77.2158 138.427 78.2886 139.169 79.9326 139.169C81.583 139.169 82.7954 138.3 82.7954 137.018C82.7954 136.015 82.1797 135.45 80.8657 135.145L79.7168 134.878C78.9678 134.701 78.6187 134.434 78.6187 134.009C78.6187 133.45 79.1963 133.076 79.958 133.076C80.7324 133.076 81.2275 133.412 81.3545 133.926H82.6621C82.5288 132.733 81.5132 132.009 79.958 132.009C78.3965 132.009 77.2729 132.873 77.2729 134.072ZM84.8711 130.403V132.162H83.7729V133.253H84.8711V137.221C84.8711 138.547 85.4741 139.081 86.9912 139.081C87.2578 139.081 87.5117 139.049 87.7339 139.011V137.925C87.5435 137.944 87.4229 137.957 87.2134 137.957C86.5342 137.957 86.2358 137.633 86.2358 136.891V133.253H87.7339V132.162H86.2358V130.403H84.8711ZM89.0796 134.072C89.0796 135.094 89.689 135.678 91.0029 135.983L92.209 136.269C92.9009 136.434 93.2246 136.713 93.2246 137.145C93.2246 137.709 92.6216 138.103 91.771 138.103C90.9458 138.103 90.438 137.767 90.2666 137.227H88.9019C89.0225 138.427 90.0952 139.169 91.7393 139.169C93.3896 139.169 94.6021 138.3 94.6021 137.018C94.6021 136.015 93.9863 135.45 92.6724 135.145L91.5234 134.878C90.7744 134.701 90.4253 134.434 90.4253 134.009C90.4253 133.45 91.0029 133.076 91.7646 133.076C92.5391 133.076 93.0342 133.412 93.1611 133.926H94.4688C94.3354 132.733 93.3198 132.009 91.7646 132.009C90.2031 132.009 89.0796 132.873 89.0796 134.072ZM97.3062 139.131C97.8457 139.131 98.2393 138.731 98.2393 138.217C98.2393 137.703 97.8457 137.303 97.3062 137.303C96.7729 137.303 96.373 137.703 96.373 138.217C96.373 138.731 96.7729 139.131 97.3062 139.131ZM103.933 132.022C102.994 132.022 102.181 132.498 101.762 133.285H101.661V132.143H100.347V141.334H101.711V137.995H101.819C102.181 138.725 102.962 139.15 103.946 139.15C105.691 139.15 106.802 137.767 106.802 135.589C106.802 133.399 105.691 132.022 103.933 132.022ZM103.546 137.982C102.403 137.982 101.686 137.062 101.686 135.589C101.686 134.11 102.403 133.19 103.552 133.19C104.708 133.19 105.399 134.091 105.399 135.589C105.399 137.087 104.708 137.982 103.546 137.982ZM108.865 141.537C110.274 141.537 110.928 141.01 111.519 139.354L114.096 132.143H112.648L110.922 137.665H110.814L109.081 132.143H107.596L110.097 139.055L109.995 139.411C109.76 140.141 109.392 140.414 108.745 140.414C108.618 140.414 108.408 140.407 108.3 140.388V141.505C108.427 141.524 108.751 141.537 108.865 141.537Z" fill="var(--vscode-foreground, #A0A0A0)" /> + <path d="M38.08 159.052L33.712 154.7L34.336 154.076L39.008 158.748V159.372L34.336 164.028L33.712 163.42L38.08 159.068V159.052Z" fill="var(--vscode-foreground, #808080)" /> + <rect x="52" y="155.536" width="70" height="7" rx="3.5" fill="var(--vscode-foreground, #FFFFFF)" fill-opacity="0.16" /> + <path d="M38.08 183.052L33.712 178.7L34.336 178.076L39.008 182.748V183.372L34.336 188.028L33.712 187.42L38.08 183.068V183.052Z" fill="var(--vscode-foreground, #808080)" /> + <rect x="52" y="179.536" width="93" height="7" rx="3.5" fill="var(--vscode-foreground, #FFFFFF)" fill-opacity="0.16" /> + <path d="M35.76 206.716H34.5C34.06 206.716 33.7067 206.849 33.44 207.116C33.1867 207.369 33.06 207.716 33.06 208.156V209.276C33.06 209.409 33 209.476 32.88 209.476H32.32C31.7333 209.476 31.3133 209.223 31.06 208.716C30.86 208.303 30.76 207.929 30.76 207.596C30.68 206.769 30.74 206.083 30.94 205.536C31.1533 204.896 31.5467 204.523 32.12 204.416H35.74C35.8733 204.416 35.94 204.396 35.94 204.356V204.036L35.86 203.996C35.8067 203.983 35.7733 203.976 35.76 203.976H33.62C33.5267 203.976 33.46 203.956 33.42 203.916C33.3933 203.876 33.38 203.809 33.38 203.716V202.916C33.38 202.449 33.5667 202.156 33.94 202.036C34.3533 201.863 34.6667 201.756 34.88 201.716C35.68 201.583 36.4267 201.603 37.12 201.776C37.4667 201.856 37.76 201.983 38 202.156C38.1467 202.303 38.2467 202.436 38.3 202.556C38.38 202.703 38.4067 202.863 38.38 203.036V205.276C38.38 205.716 38.26 206.056 38.02 206.296C37.78 206.536 37.44 206.656 37 206.656C36.7067 206.696 36.2933 206.716 35.76 206.716ZM34 202.976C34 203.109 34.0467 203.229 34.14 203.336C34.2333 203.429 34.3467 203.476 34.48 203.476C34.6133 203.476 34.7333 203.423 34.84 203.316C34.9467 203.209 35 203.096 35 202.976C35 202.856 34.9467 202.749 34.84 202.656C34.7467 202.563 34.6333 202.503 34.5 202.476C34.3533 202.476 34.2333 202.529 34.14 202.636C34.0467 202.729 34 202.843 34 202.976ZM36.26 207.356H37.5C37.94 207.356 38.2867 207.229 38.54 206.976C38.8067 206.709 38.94 206.356 38.94 205.916V204.776C38.94 204.656 39 204.596 39.12 204.596H39.68C40.2667 204.596 40.6867 204.849 40.94 205.356C41.1533 205.769 41.26 206.143 41.26 206.476C41.3267 207.303 41.26 207.989 41.06 208.536C40.8467 209.176 40.4533 209.549 39.88 209.656H36.26C36.1267 209.656 36.06 209.676 36.06 209.716V210.036L36.14 210.076C36.1933 210.089 36.2333 210.096 36.26 210.096H38.38C38.4733 210.096 38.5333 210.116 38.56 210.156C38.6 210.196 38.62 210.263 38.62 210.356V211.156C38.62 211.623 38.4333 211.916 38.06 212.036C37.6467 212.196 37.3333 212.303 37.12 212.356C36.32 212.489 35.5733 212.463 34.88 212.276C34.5333 212.209 34.24 212.089 34 211.916C33.8533 211.769 33.7533 211.636 33.7 211.516C33.62 211.369 33.5933 211.209 33.62 211.036V208.776C33.62 208.349 33.74 208.016 33.98 207.776C34.22 207.536 34.56 207.416 35 207.416C35.2933 207.376 35.7133 207.356 36.26 207.356ZM38 211.096C38 210.963 37.9533 210.849 37.86 210.756C37.7667 210.649 37.6533 210.596 37.52 210.596C37.3867 210.596 37.2667 210.649 37.16 210.756C37.0533 210.863 37 210.976 37 211.096C37 211.216 37.0467 211.323 37.14 211.416C37.2467 211.509 37.3667 211.569 37.5 211.596C37.6467 211.596 37.7667 211.549 37.86 211.456C37.9533 211.349 38 211.229 38 211.096Z" fill="#519ABA" /> + <rect x="52" y="203.536" width="63" height="7" rx="3.5" fill="var(--vscode-foreground, #FFFFFF)" fill-opacity="0.16" /> + <path d="M35.76 230.716H34.5C34.06 230.716 33.7067 230.849 33.44 231.116C33.1867 231.369 33.06 231.716 33.06 232.156V233.276C33.06 233.409 33 233.476 32.88 233.476H32.32C31.7333 233.476 31.3133 233.223 31.06 232.716C30.86 232.303 30.76 231.929 30.76 231.596C30.68 230.769 30.74 230.083 30.94 229.536C31.1533 228.896 31.5467 228.523 32.12 228.416H35.74C35.8733 228.416 35.94 228.396 35.94 228.356V228.036L35.86 227.996C35.8067 227.983 35.7733 227.976 35.76 227.976H33.62C33.5267 227.976 33.46 227.956 33.42 227.916C33.3933 227.876 33.38 227.809 33.38 227.716V226.916C33.38 226.449 33.5667 226.156 33.94 226.036C34.3533 225.863 34.6667 225.756 34.88 225.716C35.68 225.583 36.4267 225.603 37.12 225.776C37.4667 225.856 37.76 225.983 38 226.156C38.1467 226.303 38.2467 226.436 38.3 226.556C38.38 226.703 38.4067 226.863 38.38 227.036V229.276C38.38 229.716 38.26 230.056 38.02 230.296C37.78 230.536 37.44 230.656 37 230.656C36.7067 230.696 36.2933 230.716 35.76 230.716ZM34 226.976C34 227.109 34.0467 227.229 34.14 227.336C34.2333 227.429 34.3467 227.476 34.48 227.476C34.6133 227.476 34.7333 227.423 34.84 227.316C34.9467 227.209 35 227.096 35 226.976C35 226.856 34.9467 226.749 34.84 226.656C34.7467 226.563 34.6333 226.503 34.5 226.476C34.3533 226.476 34.2333 226.529 34.14 226.636C34.0467 226.729 34 226.843 34 226.976ZM36.26 231.356H37.5C37.94 231.356 38.2867 231.229 38.54 230.976C38.8067 230.709 38.94 230.356 38.94 229.916V228.776C38.94 228.656 39 228.596 39.12 228.596H39.68C40.2667 228.596 40.6867 228.849 40.94 229.356C41.1533 229.769 41.26 230.143 41.26 230.476C41.3267 231.303 41.26 231.989 41.06 232.536C40.8467 233.176 40.4533 233.549 39.88 233.656H36.26C36.1267 233.656 36.06 233.676 36.06 233.716V234.036L36.14 234.076C36.1933 234.089 36.2333 234.096 36.26 234.096H38.38C38.4733 234.096 38.5333 234.116 38.56 234.156C38.6 234.196 38.62 234.263 38.62 234.356V235.156C38.62 235.623 38.4333 235.916 38.06 236.036C37.6467 236.196 37.3333 236.303 37.12 236.356C36.32 236.489 35.5733 236.463 34.88 236.276C34.5333 236.209 34.24 236.089 34 235.916C33.8533 235.769 33.7533 235.636 33.7 235.516C33.62 235.369 33.5933 235.209 33.62 235.036V232.776C33.62 232.349 33.74 232.016 33.98 231.776C34.22 231.536 34.56 231.416 35 231.416C35.2933 231.376 35.7133 231.356 36.26 231.356ZM38 235.096C38 234.963 37.9533 234.849 37.86 234.756C37.7667 234.649 37.6533 234.596 37.52 234.596C37.3867 234.596 37.2667 234.649 37.16 234.756C37.0533 234.863 37 234.976 37 235.096C37 235.216 37.0467 235.323 37.14 235.416C37.2467 235.509 37.3667 235.569 37.5 235.596C37.6467 235.596 37.7667 235.549 37.86 235.456C37.9533 235.349 38 235.229 38 235.096Z" fill="#519ABA" /> + <rect x="52" y="227.536" width="47" height="7" rx="3.5" fill="var(--vscode-foreground, #FFFFFF)" fill-opacity="0.16" /> + <path d="M35.76 254.716H34.5C34.06 254.716 33.7067 254.849 33.44 255.116C33.1867 255.369 33.06 255.716 33.06 256.156V257.276C33.06 257.409 33 257.476 32.88 257.476H32.32C31.7333 257.476 31.3133 257.223 31.06 256.716C30.86 256.303 30.76 255.929 30.76 255.596C30.68 254.769 30.74 254.083 30.94 253.536C31.1533 252.896 31.5467 252.523 32.12 252.416H35.74C35.8733 252.416 35.94 252.396 35.94 252.356V252.036L35.86 251.996C35.8067 251.983 35.7733 251.976 35.76 251.976H33.62C33.5267 251.976 33.46 251.956 33.42 251.916C33.3933 251.876 33.38 251.809 33.38 251.716V250.916C33.38 250.449 33.5667 250.156 33.94 250.036C34.3533 249.863 34.6667 249.756 34.88 249.716C35.68 249.583 36.4267 249.603 37.12 249.776C37.4667 249.856 37.76 249.983 38 250.156C38.1467 250.303 38.2467 250.436 38.3 250.556C38.38 250.703 38.4067 250.863 38.38 251.036V253.276C38.38 253.716 38.26 254.056 38.02 254.296C37.78 254.536 37.44 254.656 37 254.656C36.7067 254.696 36.2933 254.716 35.76 254.716ZM34 250.976C34 251.109 34.0467 251.229 34.14 251.336C34.2333 251.429 34.3467 251.476 34.48 251.476C34.6133 251.476 34.7333 251.423 34.84 251.316C34.9467 251.209 35 251.096 35 250.976C35 250.856 34.9467 250.749 34.84 250.656C34.7467 250.563 34.6333 250.503 34.5 250.476C34.3533 250.476 34.2333 250.529 34.14 250.636C34.0467 250.729 34 250.843 34 250.976ZM36.26 255.356H37.5C37.94 255.356 38.2867 255.229 38.54 254.976C38.8067 254.709 38.94 254.356 38.94 253.916V252.776C38.94 252.656 39 252.596 39.12 252.596H39.68C40.2667 252.596 40.6867 252.849 40.94 253.356C41.1533 253.769 41.26 254.143 41.26 254.476C41.3267 255.303 41.26 255.989 41.06 256.536C40.8467 257.176 40.4533 257.549 39.88 257.656H36.26C36.1267 257.656 36.06 257.676 36.06 257.716V258.036L36.14 258.076C36.1933 258.089 36.2333 258.096 36.26 258.096H38.38C38.4733 258.096 38.5333 258.116 38.56 258.156C38.6 258.196 38.62 258.263 38.62 258.356V259.156C38.62 259.623 38.4333 259.916 38.06 260.036C37.6467 260.196 37.3333 260.303 37.12 260.356C36.32 260.489 35.5733 260.463 34.88 260.276C34.5333 260.209 34.24 260.089 34 259.916C33.8533 259.769 33.7533 259.636 33.7 259.516C33.62 259.369 33.5933 259.209 33.62 259.036V256.776C33.62 256.349 33.74 256.016 33.98 255.776C34.22 255.536 34.56 255.416 35 255.416C35.2933 255.376 35.7133 255.356 36.26 255.356ZM38 259.096C38 258.963 37.9533 258.849 37.86 258.756C37.7667 258.649 37.6533 258.596 37.52 258.596C37.3867 258.596 37.2667 258.649 37.16 258.756C37.0533 258.863 37 258.976 37 259.096C37 259.216 37.0467 259.323 37.14 259.416C37.2467 259.509 37.3667 259.569 37.5 259.596C37.6467 259.596 37.7667 259.549 37.86 259.456C37.9533 259.349 38 259.229 38 259.096Z" fill="#519ABA" /> + <rect x="52" y="251.536" width="58" height="7" rx="3.5" fill="var(--vscode-foreground, #FFFFFF)" fill-opacity="0.16" /> + <path d="M30.38 273.476V284.596H41.62V273.476H30.38ZM32.18 275.776H35.38V276.596H32.18V275.776ZM36.56 275.776H39.76V276.596H36.56V275.776ZM32.18 277.656H35.38V278.476H32.18V277.656ZM36.56 277.656H39.76V278.476H36.56V277.656ZM32.18 279.536H35.38V280.356H32.18V279.536ZM36.56 279.536H39.76V280.356H36.56V279.536ZM32.18 281.416H35.38V282.216H32.18V281.416ZM36.56 281.416H39.76V282.216H36.56V281.416Z" fill="#8DC149" /> + <rect x="52" y="275.536" width="71" height="7" rx="3.5" fill="var(--vscode-foreground, #FFFFFF)" fill-opacity="0.16" /> + <rect x="13" y="11" width="212.268" height="344" stroke="var(--vscode-activityBarBadge-background, #007ACC)" stroke-width="2" stroke-dasharray="6 4" /> + </g> + <g filter="url(#filter1_d_30514464)"> + <path d="M172.134 246.5C172.134 244.291 173.925 242.5 176.134 242.5H324.134C326.343 242.5 328.134 244.291 328.134 246.5V291.5C328.134 293.709 326.343 295.5 324.134 295.5H176.134C173.925 295.5 172.134 293.709 172.134 291.5V246.5Z" fill="var(--vscode-tab-inactiveBackground, #333333)" /> + <path d="M209.894 261.48H199.734L198.454 260.2L197.894 259.96H190.374L189.654 260.76V277.24L190.374 277.96H209.894L210.614 277.24V262.28L209.894 261.48ZM209.094 274.2V276.52H191.094V267.48H197.894L198.374 267.24L199.654 265.96H209.174L209.094 274.2ZM209.094 264.52H199.334L198.854 264.76L197.574 266.04H191.174V261.48H197.574L198.854 262.76L199.414 263H209.174L209.094 264.52Z" fill="var(--vscode-tab-activeForeground, #FFFFFF)" /> + <path d="M223.962 277.5H226.482V269.684C226.482 268.02 227.642 266.719 229.177 266.719C230.677 266.719 231.638 267.621 231.638 269.074V277.5H234.111V269.449C234.111 267.914 235.177 266.719 236.806 266.719C238.458 266.719 239.279 267.574 239.279 269.332V277.5H241.798V268.723C241.798 266.074 240.298 264.527 237.72 264.527C235.951 264.527 234.486 265.43 233.841 266.801H233.642C233.08 265.43 231.873 264.527 230.126 264.527C228.427 264.527 227.138 265.371 226.576 266.801H226.388V264.773H223.962V277.5ZM246.181 282.117C248.783 282.117 249.99 281.145 251.08 278.086L255.837 264.773H253.166L249.978 274.969H249.779L246.58 264.773H243.837L248.455 277.535L248.267 278.191C247.833 279.539 247.154 280.043 245.958 280.043C245.724 280.043 245.337 280.031 245.138 279.996V282.059C245.373 282.094 245.97 282.117 246.181 282.117ZM265.845 271.852V269.52H258.158V271.852H265.845ZM273.076 277.711C274.751 277.711 276.146 276.984 276.908 275.695H277.107V277.5H279.533V268.793C279.533 266.121 277.728 264.527 274.529 264.527C271.634 264.527 269.572 265.922 269.314 268.066H271.751C272.033 267.141 273.005 266.613 274.412 266.613C276.134 266.613 277.025 267.398 277.025 268.793V269.906L273.568 270.117C270.533 270.305 268.822 271.629 268.822 273.914C268.822 276.234 270.615 277.711 273.076 277.711ZM273.72 275.684C272.349 275.684 271.353 274.992 271.353 273.809C271.353 272.648 272.15 272.027 273.908 271.91L277.025 271.699V272.801C277.025 274.441 275.619 275.684 273.72 275.684ZM289.74 264.551C288.005 264.551 286.505 265.43 285.732 266.883H285.544V264.773H283.119V281.742H285.638V275.578H285.837C286.505 276.926 287.947 277.711 289.763 277.711C292.986 277.711 295.037 275.156 295.037 271.137C295.037 267.094 292.986 264.551 289.74 264.551ZM289.025 275.555C286.916 275.555 285.591 273.855 285.591 271.137C285.591 268.406 286.916 266.707 289.037 266.707C291.169 266.707 292.447 268.371 292.447 271.137C292.447 273.902 291.169 275.555 289.025 275.555ZM304.693 264.551C302.958 264.551 301.458 265.43 300.685 266.883H300.498V264.773H298.072V281.742H300.591V275.578H300.791C301.458 276.926 302.9 277.711 304.716 277.711C307.939 277.711 309.99 275.156 309.99 271.137C309.99 267.094 307.939 264.551 304.693 264.551ZM303.978 275.555C301.869 275.555 300.544 273.855 300.544 271.137C300.544 268.406 301.869 266.707 303.99 266.707C306.123 266.707 307.4 268.371 307.4 271.137C307.4 273.902 306.123 275.555 303.978 275.555Z" fill="var(--vscode-tab-activeForeground, #FFFFFF)" /> + </g> + <g filter="url(#filter2_d_30514464)"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M210.634 310C210.634 314.97 214.664 319 219.634 319V319C224.604 319 228.634 314.97 228.634 310V310C228.634 305.029 224.604 301 219.634 301V301C214.664 301 210.634 305.029 210.634 310" fill="#3794FF" /> + <path fill-rule="evenodd" clip-rule="evenodd" d="M220.634 309V306C220.634 305.448 220.186 305 219.634 305C219.082 305 218.634 305.448 218.634 306V309H215.634C215.082 309 214.634 309.448 214.634 310C214.634 310.552 215.082 311 215.634 311H218.634V314C218.634 314.552 219.082 315 219.634 315C220.186 315 220.634 314.552 220.634 314V311H223.634C224.186 311 224.634 310.552 224.634 310C224.634 309.448 224.186 309 223.634 309H220.634Z" fill="#FFFFFF" /> + <path fill-rule="evenodd" clip-rule="evenodd" d="M209.634 303.5V287.5L221.234 299.108H214.188L214.037 299.232L209.634 303.5Z" fill="#FFFFFF" /> + <path fill-rule="evenodd" clip-rule="evenodd" d="M210.634 289.8V301L213.603 298.131L213.763 297.992L218.799 298L210.634 289.8Z" fill="#000000" /> + </g> + <defs> + <filter id="filter0_d_30514464" x="0" y="0" width="238.268" height="370" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> + <feFlood flood-opacity="0" result="BackgroundImageFix" /> + <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> + <feOffset dy="2" /> + <feGaussianBlur stdDeviation="6" /> + <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0" /> + <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_30514464" /> + <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_30514464" result="shape" /> + </filter> + <filter id="filter1_d_30514464" x="160.134" y="232.5" width="180" height="77" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> + <feFlood flood-opacity="0" result="BackgroundImageFix" /> + <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> + <feOffset dy="2" /> + <feGaussianBlur stdDeviation="6" /> + <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0" /> + <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_30514464" /> + <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_30514464" result="shape" /> + </filter> + <filter id="filter2_d_30514464" x="207.834" y="286.7" width="22.6" height="35.1" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> + <feFlood flood-opacity="0" result="BackgroundImageFix" /> + <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> + <feOffset dy="1" /> + <feGaussianBlur stdDeviation="0.9" /> + <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.65 0" /> + <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_30514464" /> + <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_30514464" result="shape" /> + </filter> + <clipPath id="clip0_30514464"> + <rect width="198" height="318" fill="#FFFFFF" transform="translate(212 24)" /> + </clipPath> + </defs> +</svg> \ 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 @@ +<svg width="520" height="220" viewBox="0 0 520 220" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g> + <rect width="520" height="220" fill="var(--vscode-editor-background, #1E1E1E)" /> + <g clip-path="url(#clip0_1003_92672)"> + <g> + <rect width="520" height="39" fill="var(--vscode-editorGroupHeader-tabsBackground, #252526)" /> + <g clip-path="url(#clip1_1003_92672)"> + <g> + <rect width="115.654" height="39.2998" fill="var(--vscode-tab-activeBackground, #1E1E1E)" /> + <rect x="13.4741" y="15.72" width="88.7052" height="7.85995" rx="3.92998" fill="var(--vscode-editorHoverWidget-border, #2D2D2D)" /> + </g> + </g> + </g> + </g> + <g filter="url(#filter0_d_1003_92672)"> + <rect width="425" height="182" transform="translate(48 20)" fill="var(--vscode-quickInput-background, #252526)" /> + <g> + <g> + <rect x="55.5" y="27.5" width="410" height="25" fill="var(--vscode-input-background, #3C3C3C)" /> + <rect x="55.5" y="27.5" width="410" height="25" stroke="var(--vscode-focusBorder, #007FD4)" /> + </g> + </g> + <g> + <g> + <g> + <rect x="60" y="66" width="38" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #FFFFFF)" /> + <rect x="102" y="66" width="29" height="6" rx="3" fill="var(--vscode-textLink-foreground, #3794FF)" /> + <rect x="135" y="66" width="105" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #FFFFFF)" /> + </g> + </g> + <g> + <rect width="425" height="24" transform="translate(48 82)" fill="var(--vscode-quickInputList-focusBackground, #062F4A)" /> + <path d="M61.1489 89.8403V99H62.5708V95.8706H64.6909C66.4873 95.8706 67.7568 94.6392 67.7568 92.8745C67.7568 91.0718 66.5254 89.8403 64.7354 89.8403H61.1489ZM62.5708 91.04H64.3608C65.6113 91.04 66.3032 91.6875 66.3032 92.8745C66.3032 94.0234 65.5859 94.6772 64.3608 94.6772H62.5708V91.04ZM70.334 101.501C71.7432 101.501 72.397 100.974 72.9873 99.3174L75.5645 92.1064H74.1172L72.3906 97.6289H72.2827L70.5498 92.1064H69.0645L71.5654 99.019L71.4639 99.3745C71.229 100.104 70.8608 100.377 70.2134 100.377C70.0864 100.377 69.877 100.371 69.769 100.352V101.469C69.896 101.488 70.2197 101.501 70.334 101.501ZM77.4434 90.3672V92.1255H76.3452V93.2173H77.4434V97.1846C77.4434 98.5112 78.0464 99.0444 79.5635 99.0444C79.8301 99.0444 80.084 99.0127 80.3062 98.9746V97.8892C80.1157 97.9082 79.9951 97.9209 79.7856 97.9209C79.1064 97.9209 78.8081 97.5972 78.8081 96.8545V93.2173H80.3062V92.1255H78.8081V90.3672H77.4434ZM81.9756 99H83.3403V94.9629C83.3403 93.8838 83.9624 93.1665 85.0796 93.1665C86.0444 93.1665 86.5586 93.7314 86.5586 94.8677V99H87.9233V94.5439C87.9233 92.9126 87.0156 91.9795 85.543 91.9795C84.502 91.9795 83.772 92.4365 83.4355 93.2046H83.3276V89.4023H81.9756V99ZM92.7666 99.1333C94.7852 99.1333 96.0229 97.7812 96.0229 95.5532C96.0229 93.3252 94.7788 91.9731 92.7666 91.9731C90.748 91.9731 89.5039 93.3315 89.5039 95.5532C89.5039 97.7812 90.7417 99.1333 92.7666 99.1333ZM92.7666 97.9717C91.5796 97.9717 90.9131 97.0894 90.9131 95.5532C90.9131 94.0171 91.5796 93.1284 92.7666 93.1284C93.9473 93.1284 94.6201 94.0171 94.6201 95.5532C94.6201 97.083 93.9473 97.9717 92.7666 97.9717ZM97.667 99H99.0317V94.9565C99.0317 93.8457 99.6729 93.1602 100.682 93.1602C101.691 93.1602 102.174 93.7188 102.174 94.8613V99H103.539V94.5376C103.539 92.8936 102.688 91.9731 101.146 91.9731C100.104 91.9731 99.4189 92.4365 99.0825 93.1982H98.981V92.1064H97.667V99ZM111.042 94.855H112.184C113.377 94.855 114.114 95.4644 114.114 96.4419C114.114 97.394 113.327 98.0288 112.203 98.0288C111.092 98.0288 110.324 97.4512 110.242 96.5498H108.877C108.953 98.1621 110.286 99.2222 112.222 99.2222C114.133 99.2222 115.574 98.0796 115.574 96.5181C115.574 95.2612 114.787 94.4487 113.568 94.2646V94.1567C114.526 93.8901 115.218 93.1602 115.225 92.0557C115.231 90.7544 114.158 89.6182 112.267 89.6182C110.337 89.6182 109.144 90.729 109.048 92.2842H110.394C110.477 91.3257 111.143 90.7861 112.184 90.7861C113.212 90.7861 113.822 91.4272 113.822 92.2334C113.822 93.1157 113.136 93.7378 112.146 93.7378H111.042V94.855ZM117.96 99.0952C118.5 99.0952 118.894 98.6953 118.894 98.1812C118.894 97.667 118.5 97.2671 117.96 97.2671C117.427 97.2671 117.027 97.667 117.027 98.1812C117.027 98.6953 117.427 99.0952 117.96 99.0952ZM123.559 99.2158C125.851 99.2158 127.19 97.4004 127.19 94.29C127.19 91.2178 125.717 89.6182 123.616 89.6182C121.674 89.6182 120.296 90.9448 120.296 92.8047C120.296 94.5693 121.566 95.8579 123.312 95.8579C124.397 95.8579 125.273 95.3501 125.711 94.4741H125.819C125.781 96.7466 124.975 98.0161 123.572 98.0161C122.734 98.0161 122.08 97.5591 121.864 96.8037H120.43C120.703 98.2764 121.934 99.2158 123.559 99.2158ZM123.623 94.709C122.493 94.709 121.712 93.9155 121.712 92.7603C121.712 91.6558 122.537 90.8052 123.635 90.8052C124.727 90.8052 125.552 91.6685 125.552 92.7983C125.552 93.9092 124.746 94.709 123.623 94.709ZM129.475 99.0952C130.015 99.0952 130.408 98.6953 130.408 98.1812C130.408 97.667 130.015 97.2671 129.475 97.2671C128.942 97.2671 128.542 97.667 128.542 98.1812C128.542 98.6953 128.942 99.0952 129.475 99.0952ZM135.207 99.2222C137.34 99.2222 138.584 97.4639 138.584 94.4297C138.584 91.3892 137.321 89.6182 135.207 89.6182C133.081 89.6182 131.817 91.3892 131.817 94.4106C131.817 97.4575 133.068 99.2222 135.207 99.2222ZM135.207 98.0415C133.963 98.0415 133.252 96.772 133.252 94.4106C133.252 92.0811 133.976 90.8052 135.207 90.8052C136.438 90.8052 137.149 92.0684 137.149 94.4106C137.149 96.7783 136.451 98.0415 135.207 98.0415ZM147.35 99.2158C149.292 99.2158 150.67 97.8892 150.67 96.0293C150.67 94.2646 149.4 92.9761 147.661 92.9761C146.576 92.9761 145.693 93.4839 145.255 94.3662H145.147C145.173 92.043 146.011 90.8179 147.439 90.8179C148.302 90.8179 148.931 91.2622 149.108 92.0366H150.537C150.308 90.583 149.127 89.6182 147.407 89.6182C145.179 89.6182 143.783 91.4336 143.783 94.5439C143.783 97.6606 145.287 99.2158 147.35 99.2158ZM147.337 98.0288C146.246 98.0288 145.414 97.1655 145.414 96.0356C145.414 94.9185 146.22 94.1123 147.35 94.1123C148.48 94.1123 149.261 94.9185 149.261 96.0674C149.261 97.1782 148.429 98.0288 147.337 98.0288ZM156.688 99H158.059V97.1782H159.322V95.9722H158.059V89.8403H156.034C154.771 91.7383 153.425 93.9028 152.219 95.9722V97.1782H156.688V99ZM153.583 95.9087C154.523 94.3027 155.64 92.519 156.624 91.04H156.707V95.9976H153.583V95.9087ZM165.244 95.9404V94.6772H161.08V95.9404H165.244ZM170.913 99.1143C172.658 99.1143 173.769 97.7241 173.769 95.5532C173.769 93.3633 172.665 91.9858 170.913 91.9858C169.967 91.9858 169.154 92.4492 168.786 93.1982H168.678V89.4023H167.313V99H168.627V97.9082H168.729C169.142 98.6699 169.948 99.1143 170.913 99.1143ZM170.519 93.1538C171.674 93.1538 172.366 94.0615 172.366 95.5532C172.366 97.0449 171.674 97.9463 170.519 97.9463C169.37 97.9463 168.659 97.0322 168.653 95.5532C168.659 94.0742 169.376 93.1538 170.519 93.1538ZM176.118 90.875C176.594 90.875 176.981 90.4878 176.981 90.0181C176.981 89.542 176.594 89.1548 176.118 89.1548C175.642 89.1548 175.254 89.542 175.254 90.0181C175.254 90.4878 175.642 90.875 176.118 90.875ZM175.438 99H176.797V92.1064H175.438V99ZM179.285 90.3672V92.1255H178.187V93.2173H179.285V97.1846C179.285 98.5112 179.888 99.0444 181.405 99.0444C181.672 99.0444 181.926 99.0127 182.148 98.9746V97.8892C181.958 97.9082 181.837 97.9209 181.627 97.9209C180.948 97.9209 180.65 97.5972 180.65 96.8545V93.2173H182.148V92.1255H180.65V90.3672H179.285Z" fill="var(--vscode-list-activeSelectionForeground, #FFFFFF)" /> + </g> + <g> + <g> + <rect x="60" y="116" width="62.5" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #FFFFFF)" /> + <rect x="126.5" y="116" width="29" height="6" rx="3" fill="var(--vscode-textLink-foreground, #3794FF)" /> + <rect x="159.5" y="116" width="62.5" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #FFFFFF)" /> + </g> + </g> + <g> + <g> + <rect x="60" y="138" width="41" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #FFFFFF)" /> + <rect x="105" y="138" width="69" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #FFFFFF)" /> + <rect x="178" y="138" width="26" height="6" rx="3" fill="var(--vscode-textLink-foreground, #3794FF)" /> + </g> + </g> + <g> + <g> + <rect x="60" y="160" width="26" height="6" rx="3" fill="var(--vscode-textLink-foreground, #3794FF)" /> + <rect x="90" y="160" width="164" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #FFFFFF)" /> + </g> + </g> + <g> + <g> + <rect x="60" y="182" width="46" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #FFFFFF)" /> + <rect x="110" y="182" width="29" height="6" rx="3" fill="var(--vscode-textLink-foreground, #3794FF)" /> + <rect x="143" y="182" width="46" height="6" rx="3" fill="var(--vscode-editorHoverWidget-border, #FFFFFF)" /> + </g> + </g> + </g> + </g> + </g> + <defs> + <filter id="filter0_d_1003_92672" x="32" y="6" width="457" height="214" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> + <feFlood flood-opacity="0" result="BackgroundImageFix" /> + <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /> + <feOffset dy="2" /> + <feGaussianBlur stdDeviation="8" /> + <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.36 0" /> + <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1003_92672" /> + <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1003_92672" result="shape" /> + </filter> + <clipPath id="clip0_1003_92672"> + <rect width="520" height="220" fill="#FFFFFF" /> + </clipPath> + <clipPath id="clip1_1003_92672"> + <rect width="115.654" height="39.2998" fill="#FFFFFF" /> + </clipPath> + </defs> +</svg> \ 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 @@ +<svg width="522" height="342" viewBox="0 0 522 342" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g filter="url(#filter0_d)"> +<rect width="192" height="268" transform="translate(8 32)" fill="var(--vscode-quickInput-background, #252526)"/> +<path d="M25.8019 58.138H27.2078L28.5158 60.6072H29.6681L28.2266 57.9822C29.0096 57.7198 29.4723 57.0168 29.4723 56.1492C29.4723 54.9525 28.6403 54.1872 27.3457 54.1872H24.8053V60.6072H25.8019V58.138ZM25.8019 55.0236H27.2078C27.9774 55.0236 28.449 55.4552 28.449 56.1715C28.449 56.9056 28.0041 57.3193 27.2344 57.3193H25.8019V55.0236ZM31.8792 54.1872H30.8827V58.396C30.8827 59.7841 31.8748 60.7629 33.4809 60.7629C35.0959 60.7629 36.0836 59.7841 36.0836 58.396V54.1872H35.087V58.3159C35.087 59.2324 34.5042 59.8775 33.4809 59.8775C32.4621 59.8775 31.8792 59.2324 31.8792 58.3159V54.1872ZM38.6596 60.6072V55.9446H38.7308L42.0275 60.6072H42.9262V54.1872H41.9563V58.8587H41.8851L38.5884 54.1872H37.6897V60.6072H38.6596ZM51.1881 60.6072H52.2469L49.929 54.1872H48.8568L46.5388 60.6072H47.5621L48.1538 58.8721H50.6008L51.1881 60.6072ZM49.3417 55.3217H49.4173L50.3472 58.0712H48.4074L49.3417 55.3217ZM54.338 60.6072V55.9446H54.4092L57.7059 60.6072H58.6046V54.1872H57.6347V58.8587H57.5635L54.2668 54.1872H53.3681V60.6072H54.338ZM60.2196 54.1872V60.6072H62.5375C64.4506 60.6072 65.5584 59.4237 65.5584 57.3816C65.5584 55.3618 64.4417 54.1872 62.5375 54.1872H60.2196ZM61.2162 55.0459H62.4263C63.7566 55.0459 64.5441 55.9134 64.5441 57.395C64.5441 58.8898 63.7699 59.7485 62.4263 59.7485H61.2162V55.0459ZM69.4024 54.1872V60.6072H71.7204C73.6334 60.6072 74.7413 59.4237 74.7413 57.3816C74.7413 55.3618 73.6245 54.1872 71.7204 54.1872H69.4024ZM70.399 55.0459H71.6091C72.9394 55.0459 73.7269 55.9134 73.7269 57.395C73.7269 58.8898 72.9527 59.7485 71.6091 59.7485H70.399V55.0459ZM80.1691 59.7485H77.0903V57.7509H80.0045V56.9234H77.0903V55.0459H80.1691V54.1872H76.0938V60.6072H80.1691V59.7485ZM84.2488 60.6072C85.6191 60.6072 86.4422 59.922 86.4422 58.792C86.4422 57.9511 85.8638 57.3371 84.9963 57.2437V57.1681C85.628 57.0613 86.1219 56.4607 86.1219 55.7889C86.1219 54.8012 85.3967 54.1872 84.191 54.1872H81.6106V60.6072H84.2488ZM82.6071 55.0014H83.9597C84.6982 55.0014 85.1298 55.3529 85.1298 55.9624C85.1298 56.5897 84.6715 56.9189 83.7906 56.9189H82.6071V55.0014ZM82.6071 59.793V57.6797H83.9908C84.9295 57.6797 85.4234 58.0356 85.4234 58.7297C85.4234 59.4237 84.9473 59.793 84.0486 59.793H82.6071ZM88.7112 54.1872H87.7146V58.396C87.7146 59.7841 88.7068 60.7629 90.3129 60.7629C91.9279 60.7629 92.9156 59.7841 92.9156 58.396V54.1872H91.919V58.3159C91.919 59.2324 91.3362 59.8775 90.3129 59.8775C89.2941 59.8775 88.7112 59.2324 88.7112 58.3159V54.1872ZM99.9984 58.0801V57.2837H97.3646V58.089H99.0196V58.2314C99.0107 59.2146 98.29 59.8775 97.24 59.8775C96.0343 59.8775 95.278 58.9299 95.278 57.3905C95.278 55.8734 96.0299 54.9169 97.2178 54.9169C98.0942 54.9169 98.7038 55.3484 98.9529 56.1448H99.9539C99.7359 54.8501 98.6771 54.0315 97.2178 54.0315C95.4204 54.0315 94.2592 55.3529 94.2592 57.395C94.2592 59.4638 95.407 60.7629 97.2267 60.7629C98.9084 60.7629 99.9984 59.7085 99.9984 58.0801Z" fill="var(--vscode-foreground)"/> +<path d="M29.984 85.064L34.336 80.712L34.96 81.336L30.288 85.992H29.664L25.008 81.336L25.616 80.712L29.984 85.064Z" fill="#808080"/> +<path d="M50.0649 86L52.1787 79.6587H50.7505L49.3354 84.5498H49.2563L47.8149 79.6587H46.3164L48.4609 86H50.0649ZM56.7666 86H58.2036L56.002 79.6587H54.4419L52.2402 86H53.5718L54.0596 84.4619H56.2876L56.7666 86ZM55.1406 80.9463H55.2197L56.0063 83.4688H54.3452L55.1406 80.9463ZM60.6118 83.6797H61.6973L62.8794 86H64.3823L63.0464 83.4819C63.7759 83.1963 64.1978 82.4932 64.1978 81.6802C64.1978 80.4277 63.3408 79.6587 61.9478 79.6587H59.2847V86H60.6118V83.6797ZM60.6118 80.6958H61.7632C62.4224 80.6958 62.8354 81.0825 62.8354 81.6978C62.8354 82.3262 62.4443 82.6909 61.7764 82.6909H60.6118V80.6958ZM66.8608 86V79.6587H65.5337V86H66.8608ZM72.4727 86H73.9097L71.708 79.6587H70.1479L67.9463 86H69.2778L69.7656 84.4619H71.9937L72.4727 86ZM70.8467 80.9463H70.9258L71.7124 83.4688H70.0513L70.8467 80.9463ZM77.8384 86C79.1919 86 80.0312 85.3013 80.0312 84.1851C80.0312 83.3677 79.4204 82.7393 78.5811 82.6733V82.5942C79.2314 82.4976 79.728 81.9131 79.728 81.2407C79.728 80.2607 78.9941 79.6587 77.7637 79.6587H74.9907V86H77.8384ZM76.3179 80.6431H77.4253C78.0581 80.6431 78.4229 80.9507 78.4229 81.4692C78.4229 81.9966 78.0317 82.2954 77.3154 82.2954H76.3179V80.6431ZM76.3179 85.0112V83.1699H77.4692C78.2515 83.1699 78.6777 83.4863 78.6777 84.0796C78.6777 84.686 78.2646 85.0112 77.5 85.0112H76.3179ZM85.3794 84.8838H82.5581V79.6587H81.231V86H85.3794V84.8838ZM90.7847 84.9058H87.9106V83.3018H90.6221V82.2822H87.9106V80.7529H90.7847V79.6587H86.5835V86H90.7847V84.9058ZM91.8965 84.2466C91.9448 85.4199 92.9248 86.1626 94.4365 86.1626C96.0273 86.1626 97.0073 85.3804 97.0073 84.1147C97.0073 83.1348 96.458 82.5854 95.1836 82.313L94.4189 82.1504C93.6763 81.9878 93.373 81.7549 93.373 81.355C93.373 80.8584 93.8125 80.5376 94.4761 80.5376C95.1177 80.5376 95.5835 80.876 95.645 81.3945H96.8931C96.8535 80.2739 95.8647 79.4961 94.4673 79.4961C93.0039 79.4961 92.0415 80.2783 92.0415 81.4604C92.0415 82.4229 92.6084 83.0161 93.7642 83.2622L94.5903 83.4424C95.3682 83.6138 95.6846 83.8555 95.6846 84.2729C95.6846 84.7651 95.1968 85.1123 94.5068 85.1123C93.7598 85.1123 93.2412 84.7739 93.1797 84.2466H91.8965Z" fill="var(--vscode-foreground)"/> +<path d="M32.08 107.016L27.712 102.664L28.336 102.04L33.008 106.712V107.336L28.336 111.992L27.712 111.384L32.08 107.032V107.016Z" fill="#808080"/> +<rect x="46" y="103.5" width="27" height="7" rx="3.5" fill="#B180D7"/> +<rect x="79" y="103.5" width="70" height="7" rx="3.5" fill="var(--vscode-foreground)" fill-opacity="0.25"/> +<path d="M32.08 131.016L27.712 126.664L28.336 126.04L33.008 130.712V131.336L28.336 135.992L27.712 135.384L32.08 131.032V131.016Z" fill="#808080"/> +<rect x="46" y="127.5" width="27" height="7" rx="3.5" fill="#B180D7"/> +<rect x="79" y="127.5" width="61" height="7" rx="3.5" fill="var(--vscode-foreground)" fill-opacity="0.25"/> +<path d="M32.08 155.016L27.712 150.664L28.336 150.04L33.008 154.712V155.336L28.336 159.992L27.712 159.384L32.08 155.032V155.016Z" fill="#808080"/> +<rect x="46" y="151.5" width="40" height="7" rx="3.5" fill="#B180D7"/> +<rect x="92" y="151.5" width="34" height="7" rx="3.5" fill="var(--vscode-foreground)" fill-opacity="0.25"/> +<path d="M32.08 179.016L27.712 174.664L28.336 174.04L33.008 178.712V179.336L28.336 183.992L27.712 183.384L32.08 179.032V179.016Z" fill="#808080"/> +<path d="M50.5044 177.729H50.5747L51.7744 182H53.0181L54.7012 175.659H53.3301L52.3501 180.189H52.2798L51.1021 175.659H49.9814L48.8257 180.189H48.7554L47.7666 175.659H46.3867L48.0654 182H49.3179L50.5044 177.729ZM59.4033 182H60.8403L58.6387 175.659H57.0786L54.877 182H56.2085L56.6963 180.462H58.9243L59.4033 182ZM57.7773 176.946H57.8564L58.6431 179.469H56.9819L57.7773 176.946ZM64.1011 182V176.753H66.0039V175.659H60.8711V176.753H62.7739V182H64.1011ZM69.6514 182.163C71.1807 182.163 72.2969 181.235 72.4067 179.882H71.1147C70.9873 180.581 70.416 181.029 69.6558 181.029C68.6538 181.029 68.0342 180.185 68.0342 178.827C68.0342 177.469 68.6538 176.625 69.6514 176.625C70.4072 176.625 70.9829 177.109 71.1104 177.843H72.4023C72.3057 176.48 71.1543 175.496 69.6514 175.496C67.8101 175.496 66.6763 176.766 66.6763 178.827C66.6763 180.888 67.8145 182.163 69.6514 182.163ZM79.1392 182V175.659H77.812V178.234H74.9556V175.659H73.6284V182H74.9556V179.328H77.812V182H79.1392Z" fill="var(--vscode-foreground)"/> +<path d="M29.984 205.064L34.336 200.712L34.96 201.336L30.288 205.992H29.664L25.008 201.336L25.616 200.712L29.984 205.064Z" fill="#808080"/> +<path d="M49.4629 206.163C50.9922 206.163 52.1084 205.235 52.2183 203.882H50.9263C50.7988 204.581 50.2275 205.029 49.4673 205.029C48.4653 205.029 47.8457 204.185 47.8457 202.827C47.8457 201.469 48.4653 200.625 49.4629 200.625C50.2188 200.625 50.7944 201.109 50.9219 201.843H52.2139C52.1172 200.48 50.9658 199.496 49.4629 199.496C47.6216 199.496 46.4878 200.766 46.4878 202.827C46.4878 204.888 47.626 206.163 49.4629 206.163ZM57.3115 206H58.7485L56.5469 199.659H54.9868L52.7852 206H54.1167L54.6045 204.462H56.8325L57.3115 206ZM55.6855 200.946H55.7646L56.5513 203.469H54.8901L55.6855 200.946ZM63.978 204.884H61.1567V199.659H59.8296V206H63.978V204.884ZM69.3306 204.884H66.5093V199.659H65.1821V206H69.3306V204.884ZM72.5693 204.247C72.6177 205.42 73.5977 206.163 75.1094 206.163C76.7002 206.163 77.6802 205.38 77.6802 204.115C77.6802 203.135 77.1309 202.585 75.8564 202.313L75.0918 202.15C74.3491 201.988 74.0459 201.755 74.0459 201.355C74.0459 200.858 74.4854 200.538 75.1489 200.538C75.7905 200.538 76.2563 200.876 76.3179 201.395H77.5659C77.5264 200.274 76.5376 199.496 75.1401 199.496C73.6768 199.496 72.7144 200.278 72.7144 201.46C72.7144 202.423 73.2812 203.016 74.437 203.262L75.2632 203.442C76.041 203.614 76.3574 203.855 76.3574 204.273C76.3574 204.765 75.8696 205.112 75.1797 205.112C74.4326 205.112 73.9141 204.774 73.8525 204.247H72.5693ZM81.8022 206V200.753H83.7051V199.659H78.5723V200.753H80.4751V206H81.8022ZM88.2666 206H89.7036L87.502 199.659H85.9419L83.7402 206H85.0718L85.5596 204.462H87.7876L88.2666 206ZM86.6406 200.946H86.7197L87.5063 203.469H85.8452L86.6406 200.946ZM93.1885 206.163C94.7178 206.163 95.834 205.235 95.9438 203.882H94.6519C94.5244 204.581 93.9531 205.029 93.1929 205.029C92.1909 205.029 91.5713 204.185 91.5713 202.827C91.5713 201.469 92.1909 200.625 93.1885 200.625C93.9443 200.625 94.52 201.109 94.6475 201.843H95.9395C95.8428 200.48 94.6914 199.496 93.1885 199.496C91.3472 199.496 90.2134 200.766 90.2134 202.827C90.2134 204.888 91.3516 206.163 93.1885 206.163ZM98.4927 206V204.062L99.1211 203.306L100.962 206H102.553L100.079 202.436L102.391 199.659H100.914L98.5718 202.506H98.4927V199.659H97.1655V206H98.4927Z" fill="var(--vscode-foreground)"/> +<path d="M32.08 227.016L27.712 222.664L28.336 222.04L33.008 226.712V227.336L28.336 231.992L27.712 231.384L32.08 227.032V227.016Z" fill="#808080"/> +<rect x="46" y="223.5" width="70" height="7" rx="3.5" fill="var(--vscode-foreground)" fill-opacity="0.25"/> +<path d="M32.08 251.016L27.712 246.664L28.336 246.04L33.008 250.712V251.336L28.336 255.992L27.712 255.384L32.08 251.032V251.016Z" fill="#808080"/> +<rect x="46" y="247.5" width="48" height="7" rx="3.5" fill="var(--vscode-foreground)" fill-opacity="0.25"/> +<path d="M32.08 275.016L27.712 270.664L28.336 270.04L33.008 274.712V275.336L28.336 279.992L27.712 279.384L32.08 275.032V275.016Z" fill="#808080"/> +<rect x="46" y="271.5" width="46" height="7" rx="3.5" fill="var(--vscode-foreground)" fill-opacity="0.25"/> +</g> +<g filter="url(#filter1_d)"> +<rect width="379" height="318" transform="translate(131 10)" fill="var(--vscode-quickInput-background, #252526)"/> +<rect width="379" height="35" transform="translate(131 10)" fill="var(--vscode-editorGroupHeader-tabsBackground, #292929)"/> +<rect width="103" height="35" transform="translate(131 10)" fill="var(--vscode-tab-activeBackground, #1e1e1e)"/> +<rect x="143" y="24" width="79" height="7" rx="3.5" fill="var(--vscode-foreground)" fill-opacity="0.25"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M469 20.5V35.5L470 36L481.5 28.5V27.5L470 20L469 20.5ZM478.5 28L471 23V33L478.5 28Z" fill="var(--vscode-titleBar-activeForeground)"/> +<rect x="151" y="69" width="83" height="8" rx="4" fill="#75BEFF"/> +<rect x="151" y="89" width="65" height="8" rx="4" fill="#75BEFF"/> +<rect x="151" y="109" width="83" height="8" rx="4" fill="#75BEFF"/> +<rect width="379" height="24" transform="translate(131 147)" fill="var(--vscode-editor-stackFrameHighlightBackground)"/> +<path d="M151.496 158.152L147.24 153.416L146.312 153H141.256L140.008 154.248V163.736L141.256 164.984H146.312L147.24 164.552L151.496 159.816V158.152ZM146.312 163.736H141.256V154.248H146.312L150.568 158.984L146.312 163.736Z" fill="#FFFF00"/> +<path d="M173.197 162.174V165.496H172.113V156.438H173.197V157.275C173.377 156.951 173.615 156.705 173.912 156.537C174.213 156.365 174.559 156.279 174.949 156.279C175.742 156.279 176.363 156.586 176.812 157.199C177.266 157.812 177.492 158.662 177.492 159.748C177.492 160.814 177.266 161.652 176.812 162.262C176.359 162.867 175.738 163.17 174.949 163.17C174.551 163.17 174.201 163.086 173.9 162.918C173.604 162.746 173.369 162.498 173.197 162.174ZM176.361 159.725C176.361 158.889 176.229 158.258 175.963 157.832C175.701 157.406 175.311 157.193 174.791 157.193C174.268 157.193 173.871 157.408 173.602 157.838C173.332 158.264 173.197 158.893 173.197 159.725C173.197 160.553 173.332 161.182 173.602 161.611C173.871 162.041 174.268 162.256 174.791 162.256C175.311 162.256 175.701 162.043 175.963 161.617C176.229 161.191 176.361 160.561 176.361 159.725ZM184.998 157.791C184.768 157.611 184.533 157.48 184.295 157.398C184.057 157.316 183.795 157.275 183.51 157.275C182.838 157.275 182.324 157.486 181.969 157.908C181.613 158.33 181.436 158.939 181.436 159.736V163H180.352V156.438H181.436V157.721C181.615 157.256 181.891 156.9 182.262 156.654C182.637 156.404 183.08 156.279 183.592 156.279C183.857 156.279 184.105 156.312 184.336 156.379C184.566 156.445 184.787 156.549 184.998 156.689V157.791ZM187.312 156.461H190.072V162.162H192.211V163H186.855V162.162H188.994V157.299H187.312V156.461ZM188.994 153.912H190.072V155.271H188.994V153.912ZM198.85 158.934V163H197.766V158.934C197.766 158.344 197.662 157.91 197.455 157.633C197.248 157.355 196.924 157.217 196.482 157.217C195.979 157.217 195.59 157.396 195.316 157.756C195.047 158.111 194.912 158.623 194.912 159.291V163H193.834V156.438H194.912V157.422C195.104 157.047 195.363 156.764 195.691 156.572C196.02 156.377 196.408 156.279 196.857 156.279C197.525 156.279 198.023 156.5 198.352 156.941C198.684 157.379 198.85 158.043 198.85 158.934ZM203.52 154.574V156.438H205.969V157.275H203.52V160.838C203.52 161.322 203.611 161.66 203.795 161.852C203.979 162.043 204.299 162.139 204.756 162.139H205.969V163H204.65C203.842 163 203.271 162.838 202.939 162.514C202.607 162.189 202.441 161.631 202.441 160.838V157.275H200.689V156.438H202.441V154.574H203.52Z" fill="#D7BA7D"/> +<path d="M212.162 153.309C211.643 154.199 211.254 155.088 210.996 155.975C210.742 156.857 210.615 157.748 210.615 158.646C210.615 159.541 210.742 160.432 210.996 161.318C211.254 162.205 211.643 163.098 212.162 163.996H211.225C210.635 163.066 210.195 162.162 209.906 161.283C209.617 160.4 209.473 159.521 209.473 158.646C209.473 157.775 209.617 156.898 209.906 156.016C210.195 155.133 210.635 154.23 211.225 153.309H212.162ZM218.344 157.105C218.477 156.824 218.645 156.617 218.848 156.484C219.055 156.348 219.303 156.279 219.592 156.279C220.119 156.279 220.49 156.484 220.705 156.895C220.924 157.301 221.033 158.068 221.033 159.197V163H220.049V159.244C220.049 158.318 219.996 157.744 219.891 157.521C219.789 157.295 219.602 157.182 219.328 157.182C219.016 157.182 218.801 157.303 218.684 157.545C218.57 157.783 218.514 158.35 218.514 159.244V163H217.529V159.244C217.529 158.307 217.473 157.729 217.359 157.51C217.25 157.291 217.051 157.182 216.762 157.182C216.477 157.182 216.277 157.303 216.164 157.545C216.055 157.783 216 158.35 216 159.244V163H215.021V156.438H216V157C216.129 156.766 216.289 156.588 216.48 156.467C216.676 156.342 216.896 156.279 217.143 156.279C217.439 156.279 217.686 156.348 217.881 156.484C218.08 156.621 218.234 156.828 218.344 157.105ZM227.314 156.666V157.721C227.006 157.541 226.695 157.406 226.383 157.316C226.07 157.227 225.752 157.182 225.428 157.182C224.939 157.182 224.574 157.262 224.332 157.422C224.094 157.578 223.975 157.818 223.975 158.143C223.975 158.436 224.064 158.654 224.244 158.799C224.424 158.943 224.871 159.084 225.586 159.221L226.02 159.303C226.555 159.404 226.959 159.607 227.232 159.912C227.51 160.217 227.648 160.613 227.648 161.102C227.648 161.75 227.418 162.258 226.957 162.625C226.496 162.988 225.855 163.17 225.035 163.17C224.711 163.17 224.371 163.135 224.016 163.064C223.66 162.998 223.275 162.896 222.861 162.76V161.646C223.264 161.854 223.648 162.01 224.016 162.115C224.383 162.217 224.73 162.268 225.059 162.268C225.535 162.268 225.904 162.172 226.166 161.98C226.428 161.785 226.559 161.514 226.559 161.166C226.559 160.666 226.08 160.32 225.123 160.129L225.076 160.117L224.672 160.035C224.051 159.914 223.598 159.711 223.312 159.426C223.027 159.137 222.885 158.744 222.885 158.248C222.885 157.619 223.098 157.135 223.523 156.795C223.949 156.451 224.557 156.279 225.346 156.279C225.697 156.279 226.035 156.312 226.359 156.379C226.684 156.441 227.002 156.537 227.314 156.666ZM233.871 159.666C233.871 158.857 233.738 158.244 233.473 157.826C233.211 157.404 232.828 157.193 232.324 157.193C231.797 157.193 231.395 157.404 231.117 157.826C230.84 158.244 230.701 158.857 230.701 159.666C230.701 160.475 230.84 161.092 231.117 161.518C231.398 161.939 231.805 162.15 232.336 162.15C232.832 162.15 233.211 161.938 233.473 161.512C233.738 161.086 233.871 160.471 233.871 159.666ZM234.949 162.578C234.949 163.562 234.717 164.309 234.252 164.816C233.787 165.324 233.104 165.578 232.201 165.578C231.904 165.578 231.594 165.551 231.27 165.496C230.945 165.441 230.621 165.361 230.297 165.256V164.189C230.68 164.369 231.027 164.502 231.34 164.588C231.652 164.674 231.939 164.717 232.201 164.717C232.783 164.717 233.207 164.559 233.473 164.242C233.738 163.926 233.871 163.424 233.871 162.736V162.689V161.957C233.699 162.324 233.465 162.598 233.168 162.777C232.871 162.957 232.51 163.047 232.084 163.047C231.318 163.047 230.707 162.74 230.25 162.127C229.793 161.514 229.564 160.693 229.564 159.666C229.564 158.635 229.793 157.812 230.25 157.199C230.707 156.586 231.318 156.279 232.084 156.279C232.506 156.279 232.863 156.363 233.156 156.531C233.449 156.699 233.688 156.959 233.871 157.311V156.461H234.949V162.578ZM238.113 153.309H239.051C239.641 154.23 240.08 155.133 240.369 156.016C240.658 156.898 240.803 157.775 240.803 158.646C240.803 159.525 240.658 160.406 240.369 161.289C240.08 162.172 239.641 163.074 239.051 163.996H238.113C238.633 163.09 239.02 162.193 239.273 161.307C239.531 160.42 239.66 159.533 239.66 158.646C239.66 157.756 239.531 156.867 239.273 155.98C239.02 155.094 238.633 154.203 238.113 153.309Z" fill="var(--vscode-foreground)"/> +<circle cx="145" cy="159" r="2" fill="#FF0000"/> +<rect x="151" y="201" width="65" height="8" rx="4" fill="#75BEFF"/> +<rect x="151" y="221" width="47" height="8" rx="4" fill="#D7BA7D"/> +<rect x="151" y="273" width="31" height="8" rx="4" fill="#D7BA7D"/> +<rect x="240" y="69" width="42" height="8" rx="4" fill="#CC6633"/> +<rect x="222" y="89" width="42" height="8" rx="4" fill="#CC6633"/> +<rect x="240" y="109" width="42" height="8" rx="4" fill="#D7BA7D"/> +<rect x="222" y="201" width="78" height="8" rx="4" fill="#D7BA7D"/> +<rect x="204" y="221" width="73" height="8" rx="4" fill="#CC6633"/> +<rect x="188" y="273" width="54" height="8" rx="4" fill="#CC6633"/> +</g> +<g filter="url(#filter2_d)"> +<rect x="383" y="212" width="98" height="88" rx="8" fill="var(--vscode-quickInput-background, #252526)"/> +<path d="M415.516 270V258.496H427.332V254.395H415.516V246.055H428.445V241.816H410.477V270H415.516ZM443.738 270.703C449.891 270.703 454.012 266.66 454.012 260.762C454.012 255.098 450.105 251.484 445.281 251.484C442.41 251.484 440.34 252.461 439.09 254.199H438.758L439.539 245.957H452.508V241.816H435.613L434.07 258.203H438.641C439.598 256.348 441.453 255.234 443.855 255.234C446.961 255.234 449.148 257.48 449.148 260.84C449.148 264.297 446.961 266.602 443.719 266.602C440.848 266.602 438.68 264.863 438.367 262.227H433.66C433.875 267.168 437.918 270.703 443.738 270.703Z" fill="var(--vscode-input-foreground)"/> +</g> +<defs> +<filter id="filter0_d" x="0" y="26" width="208" height="284" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="2"/> +<feGaussianBlur stdDeviation="4"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.4 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/> +</filter> +<filter id="filter1_d" x="119" y="0" width="403" height="342" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="2"/> +<feGaussianBlur stdDeviation="6"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/> +</filter> +<filter id="filter2_d" x="371" y="202" width="122" height="112" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="2"/> +<feGaussianBlur stdDeviation="6"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/> +</filter> +</defs> +</svg> diff --git a/schemas/conda-environment.json b/schemas/conda-environment.json index 86ea60213263..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": { @@ -41,7 +41,7 @@ } } }, - "required": [ "pip" ] + "required": ["pip"] } ] } 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 4862680191f1..000000000000 --- a/snippets/python.json +++ /dev/null @@ -1,243 +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" - }, -} diff --git a/sprint-planning.github-issues b/sprint-planning.github-issues new file mode 100644 index 000000000000..1fbd09a790e8 --- /dev/null +++ b/sprint-planning.github-issues @@ -0,0 +1,72 @@ +[ + { + "kind": 1, + "language": "markdown", + "value": "# Query constants" + }, + { + "kind": 2, + "language": "github-issues", + "value": "$pvsc=repo:microsoft/vscode-python\n$open=is:open\n$upvotes=sort:reactions-+1-desc" + }, + { + "kind": 1, + "language": "markdown", + "value": "# Priority issues 🚨" + }, + { + "kind": 1, + "language": "markdown", + "value": "## Important/P1" + }, + { + "kind": 2, + "language": "github-issues", + "value": "$pvsc $open label:\"important\"" + }, + { + "kind": 1, + "language": "markdown", + "value": "# Regressions 🔙" + }, + { + "kind": 2, + "language": "github-issues", + "value": "$pvsc $open label:\"regression\"" + }, + { + "kind": 1, + "language": "markdown", + "value": "# Partner asks" + }, + { + "kind": 2, + "language": "github-issues", + "value": "$pvsc $open label:\"partner ask\"" + }, + { + "kind": 1, + "language": "markdown", + "value": "# Upvotes 👍" + }, + { + "kind": 1, + "language": "markdown", + "value": "## Enhancements 💪" + }, + { + "kind": 2, + "language": "github-issues", + "value": "$pvsc $open $upvotes label:\"feature-request\" " + }, + { + "kind": 1, + "language": "markdown", + "value": "## Bugs 🐜" + }, + { + "kind": 2, + "language": "github-issues", + "value": "$pvsc $open $upvotes label:\"bug\"" + } +] diff --git a/src/client/activation/activationManager.ts b/src/client/activation/activationManager.ts new file mode 100644 index 000000000000..9e97c5c48857 --- /dev/null +++ b/src/client/activation/activationManager.ts @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, multiInject } from 'inversify'; +import { TextDocument } from 'vscode'; +import { IApplicationDiagnostics } from '../application/types'; +import { IActiveResourceService, IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { PYTHON_LANGUAGE } from '../common/constants'; +import { IFileSystem } from '../common/platform/types'; +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<string>(); + + protected readonly isInterpreterSetForWorkspacePromises = new Map<string, Deferred<void>>(); + + private readonly disposables: IDisposable[] = []; + + private docOpenedHandler?: IDisposable; + + constructor( + @multiInject(IExtensionActivationService) private activationServices: IExtensionActivationService[], + @multiInject(IExtensionSingleActivationService) + private singleActivationServices: IExtensionSingleActivationService[], + @inject(IDocumentManager) private readonly documentManager: IDocumentManager, + @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(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService, + ) {} + + 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(); + } + if (this.docOpenedHandler) { + this.docOpenedHandler.dispose(); + this.docOpenedHandler = undefined; + } + } + + public async activate(startupStopWatch: StopWatch): Promise<void> { + this.filterServices(); + await this.initialize(); + + // Activate all activation services together. + + await Promise.all([ + ...this.singleActivationServices.map((item) => item.activate()), + this.activateWorkspace(this.activeResourceService.getActiveResource(), startupStopWatch), + ]); + } + + @traceDecoratorError('Failed to activate a workspace') + public async activateWorkspace(resource: Resource, startupStopWatch?: StopWatch): Promise<void> { + 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.workspaceService.isTrusted) { + // Do not interact with interpreters in a untrusted workspace. + await this.autoSelection.autoSelectInterpreter(resource); + await this.interpreterPathService.copyOldInterpreterStorageValuesToNew(resource); + } + await sendActivationTelemetry(this.fileSystem, this.workspaceService, resource); + await Promise.all(this.activationServices.map((item) => item.activate(resource, startupStopWatch))); + await this.appDiagnostics.performPreStartupHealthCheck(resource); + } + + public async initialize(): Promise<void> { + this.addHandlers(); + this.addRemoveDocOpenedHandlers(); + } + + 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 === '' && hasWorkspaceFolders) { + return; + } + if (this.activatedWorkspaces.has(key)) { + return; + } + this.activateWorkspace(doc.uri).ignoreErrors(); + } + + protected addHandlers(): void { + this.disposables.push(this.workspaceService.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); + } + + protected addRemoveDocOpenedHandlers(): void { + if (this.hasMultipleWorkspaces()) { + if (!this.docOpenedHandler) { + this.docOpenedHandler = this.documentManager.onDidOpenTextDocument(this.onDocOpened, this); + } + return; + } + if (this.docOpenedHandler) { + this.docOpenedHandler.dispose(); + this.docOpenedHandler = undefined; + } + } + + protected onWorkspaceFoldersChanged(): void { + // If an activated workspace folder was removed, delete its key + const workspaceKeys = this.workspaceService.workspaceFolders!.map((workspaceFolder) => + this.getWorkspaceKey(workspaceFolder.uri), + ); + const activatedWkspcKeys = Array.from(this.activatedWorkspaces.keys()); + const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter((item) => workspaceKeys.indexOf(item) < 0); + if (activatedWkspcFoldersRemoved.length > 0) { + for (const folder of activatedWkspcFoldersRemoved) { + this.activatedWorkspaces.delete(folder); + } + } + this.addRemoveDocOpenedHandlers(); + } + + protected hasMultipleWorkspaces(): boolean { + return (this.workspaceService.workspaceFolders?.length || 0) > 1; + } + + 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 5100bb44e3f4..000000000000 --- a/src/client/activation/activationService.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 { - ConfigurationChangeEvent, - Disposable, OutputChannel, Uri -} from 'vscode'; -import { LSNotSupportedDiagnosticServiceId } from '../application/diagnostics/checks/lsNotSupported'; -import { IDiagnosticsService } from '../application/diagnostics/types'; -import { - IApplicationShell, ICommandManager, - IWorkspaceService -} from '../common/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; -import '../common/extensions'; -import { - IConfigurationService, IDisposableRegistry, - IOutputChannel, IPythonSettings -} from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { PYTHON_LANGUAGE_SERVER_PLATFORM_NOT_SUPPORTED } from '../telemetry/constants'; -import { - ExtensionActivators, IExtensionActivationService, - IExtensionActivator -} from './types'; - -const jediEnabledSetting: keyof IPythonSettings = 'jediEnabled'; -type ActivatorInfo = { jedi: boolean; activator: IExtensionActivator }; - -@injectable() -export class ExtensionActivationService implements IExtensionActivationService, Disposable { - private currentActivator?: ActivatorInfo; - private readonly workspaceService: IWorkspaceService; - private readonly output: OutputChannel; - private readonly appShell: IApplicationShell; - private readonly lsNotSupportedDiagnosticService: IDiagnosticsService; - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - this.output = this.serviceContainer.get<OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell); - this.lsNotSupportedDiagnosticService = this.serviceContainer.get<IDiagnosticsService>(IDiagnosticsService, LSNotSupportedDiagnosticServiceId); - const disposables = serviceContainer.get<IDisposableRegistry>(IDisposableRegistry); - disposables.push(this); - disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); - } - - public async activate(): Promise<void> { - if (this.currentActivator) { - return; - } - - let jedi = this.useJedi(); - if (!jedi) { - const diagnostic = await this.lsNotSupportedDiagnosticService.diagnose(); - this.lsNotSupportedDiagnosticService.handle(diagnostic).ignoreErrors(); - if (diagnostic.length){ - sendTelemetryEvent(PYTHON_LANGUAGE_SERVER_PLATFORM_NOT_SUPPORTED); - jedi = true; - } - } - - await this.logStartup(jedi); - - const activatorName = jedi ? ExtensionActivators.Jedi : ExtensionActivators.DotNet; - const activator = this.serviceContainer.get<IExtensionActivator>(IExtensionActivator, activatorName); - this.currentActivator = { jedi, activator }; - - await activator.activate(); - } - - public dispose() { - if (this.currentActivator) { - this.currentActivator.activator.deactivate().ignoreErrors(); - } - } - - private async logStartup(isJedi: boolean): Promise<void> { - const outputLine = isJedi ? 'Starting Jedi Python language engine.' : 'Starting Microsoft Python language server.'; - this.output.appendLine(outputLine); - } - - 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.${jediEnabledSetting}`, uri)) === -1) { - return; - } - const jedi = this.useJedi(); - if (this.currentActivator && this.currentActivator.jedi === jedi) { - return; - } - - const item = await this.appShell.showInformationMessage('Please reload the window switching between language engines.', 'Reload'); - if (item === 'Reload') { - this.serviceContainer.get<ICommandManager>(ICommandManager).executeCommand('workbench.action.reloadWindow'); - } - } - private useJedi(): boolean { - const workspacesUris: (Uri | undefined)[] = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders!.map(item => item.uri) : [undefined]; - const configuraionService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); - return workspacesUris.filter(uri => configuraionService.getSettings(uri).jediEnabled).length > 0; - } -} diff --git a/src/client/activation/commands.ts b/src/client/activation/commands.ts new file mode 100644 index 000000000000..158d9662ec46 --- /dev/null +++ b/src/client/activation/commands.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +export namespace Commands { + export const RestartLS = 'python.analysis.restartLanguageServer'; +} diff --git a/src/client/activation/common/analysisOptions.ts b/src/client/activation/common/analysisOptions.ts new file mode 100644 index 000000000000..75d0aabef9d2 --- /dev/null +++ b/src/client/activation/common/analysisOptions.ts @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +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 { ILogOutputChannel, Resource } from '../../common/types'; +import { debounceSync } from '../../common/utils/decorators'; +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { traceDecoratorError } from '../../logging'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { ILanguageServerAnalysisOptions, ILanguageServerOutputChannel } from '../types'; + +export abstract class LanguageServerAnalysisOptionsBase implements ILanguageServerAnalysisOptions { + protected readonly didChange = new EventEmitter<void>(); + private readonly output: ILogOutputChannel; + + protected constructor( + lsOutputChannel: ILanguageServerOutputChannel, + protected readonly workspace: IWorkspaceService, + ) { + this.output = lsOutputChannel.channel; + } + + public async initialize(_resource: Resource, _interpreter: PythonEnvironment | undefined) {} + + public get onDidChange(): Event<void> { + return this.didChange.event; + } + + public dispose(): void { + this.didChange.dispose(); + } + + @traceDecoratorError('Failed to get analysis options') + public async getAnalysisOptions(): Promise<LanguageClientOptions> { + const workspaceFolder = this.getWorkspaceFolder(); + const documentSelector = this.getDocumentFilters(workspaceFolder); + + return { + documentSelector, + workspaceFolder, + synchronize: { + configurationSection: this.getConfigSectionsToSynchronize(), + }, + outputChannel: this.output, + revealOutputChannelOn: RevealOutputChannelOn.Never, + initializationOptions: await this.getInitializationOptions(), + }; + } + + protected getWorkspaceFolder(): WorkspaceFolder | undefined { + return undefined; + } + + protected getDocumentFilters(_workspaceFolder?: WorkspaceFolder): DocumentFilter[] { + return this.workspace.isVirtualWorkspace ? [{ language: PYTHON_LANGUAGE }] : PYTHON; + } + + protected getConfigSectionsToSynchronize(): string[] { + return [PYTHON_LANGUAGE]; + } + + protected async getInitializationOptions(): Promise<any> { + 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<string> { + const vars = await this.envVarsProvider.getEnvironmentVariables(); + this.envPythonPath = vars.PYTHONPATH || ''; + return this.envPythonPath; + } + + @debounceSync(1000) + protected onEnvVarChange(): void { + this.notifyifEnvPythonPathChanged().ignoreErrors(); + } + + protected async notifyifEnvPythonPathChanged(): Promise<void> { + const vars = await this.envVarsProvider.getEnvironmentVariables(); + const envPythonPath = vars.PYTHONPATH || ''; + + if (this.envPythonPath !== envPythonPath) { + this.didChange.fire(); + } + } +} diff --git a/src/client/activation/common/cancellationUtils.ts b/src/client/activation/common/cancellationUtils.ts new file mode 100644 index 000000000000..d14307174107 --- /dev/null +++ b/src/client/activation/common/cancellationUtils.ts @@ -0,0 +1,100 @@ +/* eslint-disable max-classes-per-file */ +/* + * cancellationUtils.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * Helper methods around cancellation + */ + +import { randomBytes } from 'crypto'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { + CancellationReceiverStrategy, + CancellationSenderStrategy, + CancellationStrategy, + Disposable, + MessageConnection, +} from 'vscode-languageclient/node'; + +type CancellationId = string | number; + +function getCancellationFolderPath(folderName: string) { + return path.join(os.tmpdir(), 'python-languageserver-cancellation', folderName); +} + +function getCancellationFilePath(folderName: string, id: CancellationId) { + return path.join(getCancellationFolderPath(folderName), `cancellation-${String(id)}.tmp`); +} + +function tryRun(callback: () => void) { + try { + callback(); + } catch (e) { + // No body. + } +} + +class FileCancellationSenderStrategy implements CancellationSenderStrategy { + constructor(readonly folderName: string) { + const folder = getCancellationFolderPath(folderName)!; + tryRun(() => fs.mkdirSync(folder, { recursive: true })); + } + + public async sendCancellation(_: MessageConnection, id: CancellationId) { + const file = getCancellationFilePath(this.folderName, id); + tryRun(() => fs.writeFileSync(file, '', { flag: 'w' })); + } + + public cleanup(id: CancellationId): void { + tryRun(() => fs.unlinkSync(getCancellationFilePath(this.folderName, id))); + } + + public dispose(): void { + const folder = getCancellationFolderPath(this.folderName); + tryRun(() => rimraf(folder)); + + function rimraf(location: string) { + const stat = fs.lstatSync(location); + if (stat) { + if (stat.isDirectory() && !stat.isSymbolicLink()) { + for (const dir of fs.readdirSync(location)) { + rimraf(path.join(location, dir)); + } + + fs.rmdirSync(location); + } else { + fs.unlinkSync(location); + } + } + } + } +} + +export class FileBasedCancellationStrategy implements CancellationStrategy, Disposable { + private _sender: FileCancellationSenderStrategy; + + constructor() { + const folderName = randomBytes(21).toString('hex'); + this._sender = new FileCancellationSenderStrategy(folderName); + } + + // eslint-disable-next-line class-methods-use-this + get receiver(): CancellationReceiverStrategy { + return CancellationReceiverStrategy.Message; + } + + get sender(): CancellationSenderStrategy { + return this._sender; + } + + public getCommandLineArguments(): string[] { + return [`--cancellationReceive=file:${this._sender.folderName}`]; + } + + public dispose(): void { + this._sender.dispose(); + } +} 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<void> { + const lsType = await getDefaultLanguageServer(extensions); + serviceManager.addSingletonInstance<IDefaultLanguageServer>( + IDefaultLanguageServer, + new DefaultLanguageServer(lsType), + ); +} + +async function getDefaultLanguageServer(extensions: IExtensions): Promise<DefaultLSType> { + if (extensions.getExtension(PYLANCE_EXTENSION_ID)) { + return LanguageServerType.Node; + } + + return LanguageServerType.Jedi; +} diff --git a/src/client/activation/common/languageServerChangeHandler.ts b/src/client/activation/common/languageServerChangeHandler.ts new file mode 100644 index 000000000000..83ff204ed6e7 --- /dev/null +++ b/src/client/activation/common/languageServerChangeHandler.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ConfigurationTarget, Disposable } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../../common/constants'; +import { IConfigurationService, IExtensions } from '../../common/types'; +import { createDeferred } from '../../common/utils/async'; +import { Pylance } from '../../common/utils/localize'; +import { LanguageServerType } from '../types'; + +export async function promptForPylanceInstall( + appShell: IApplicationShell, + commandManager: ICommandManager, + workspace: IWorkspaceService, + configService: IConfigurationService, +): Promise<void> { + const response = await appShell.showWarningMessage( + Pylance.pylanceRevertToJediPrompt, + Pylance.pylanceInstallPylance, + Pylance.pylanceRevertToJedi, + Pylance.remindMeLater, + ); + + if (response === Pylance.pylanceInstallPylance) { + commandManager.executeCommand('extension.open', PYLANCE_EXTENSION_ID); + } else if (response === Pylance.pylanceRevertToJedi) { + const inspection = workspace.getConfiguration('python').inspect<string>('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); + } + } +} + +// Tracks language server type and issues appropriate reload or install prompts. +export class LanguageServerChangeHandler implements Disposable { + // For tests that need to track Pylance install completion. + private readonly pylanceInstallCompletedDeferred = createDeferred<void>(); + + private readonly disposables: Disposable[] = []; + + private pylanceInstalled = false; + + constructor( + private currentLsType: LanguageServerType | undefined, + private readonly extensions: IExtensions, + private readonly appShell: IApplicationShell, + 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(); + }), + ); + } + + public dispose(): void { + while (this.disposables.length) { + this.disposables.pop()?.dispose(); + } + } + + // For tests that need to track Pylance install completion. + get pylanceInstallCompleted(): Promise<void> { + return this.pylanceInstallCompletedDeferred.promise; + } + + public async handleLanguageServerChange(lsType: LanguageServerType | undefined): Promise<void> { + if (this.currentLsType === lsType || lsType === LanguageServerType.Microsoft) { + return; + } + // VS Code has to be reloaded when language server type changes. In case of Pylance + // it also has to be installed manually by the user. We avoid prompting to reload + // if target changes to Pylance when Pylance is not installed since otherwise user + // 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. + if (lsType === LanguageServerType.Node && !this.isPylanceInstalled()) { + // If not installed, point user to Pylance at the store. + 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. + } + + this.currentLsType = lsType; + } + + private async extensionsChangeHandler(): Promise<void> { + // 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(); + } + } + + private isPylanceInstalled(): boolean { + return !!this.extensions.getExtension(PYLANCE_EXTENSION_ID); + } +} 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<void> { + 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/downloadChannelRules.ts b/src/client/activation/downloadChannelRules.ts deleted file mode 100644 index 3ef61bc160ac..000000000000 --- a/src/client/activation/downloadChannelRules.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 { 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<boolean> { - return true; - } -} -@injectable() -export class DownloadStableChannelRule implements IDownloadChannelRule { - public async shouldLookForNewLanguageServer(currentFolder?: FolderVersionPair): Promise<boolean> { - return currentFolder ? false : true; - } -} -@injectable() -export class DownloadBetaChannelRule implements IDownloadChannelRule { - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { } - public async shouldLookForNewLanguageServer(currentFolder?: FolderVersionPair): Promise<boolean> { - // For beta, we do this only once a day. - const stateFactory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory); - const globalState = stateFactory.createGlobalPersistentState<boolean>(lastCheckedForLSDateTimeCacheKey, - true, - frequencyForBetalLSDownloadCheck); - - // If we haven't 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/downloader.ts b/src/client/activation/downloader.ts deleted file mode 100644 index 32c49369ba6f..000000000000 --- a/src/client/activation/downloader.ts +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import { ProgressLocation, window } from 'vscode'; -import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; -import { IFileSystem } from '../common/platform/types'; -import { IExtensionContext, IOutputChannel } from '../common/types'; -import { createDeferred } from '../common/utils/async'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { - PYTHON_LANGUAGE_SERVER_DOWNLOADED, - PYTHON_LANGUAGE_SERVER_EXTRACTED -} from '../telemetry/constants'; -import { PlatformData } from './platformData'; -import { IHttpClient, ILanguageServerDownloader, ILanguageServerFolderService } from './types'; - -const downloadFileExtension = '.nupkg'; - -export class LanguageServerDownloader implements ILanguageServerDownloader { - private readonly output: IOutputChannel; - private readonly fs: IFileSystem; - constructor( - private readonly platformData: PlatformData, - private readonly engineFolder: string, - private readonly serviceContainer: IServiceContainer - ) { - this.output = this.serviceContainer.get<IOutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem); - - } - - public async getDownloadInfo() { - const lsFolderService = this.serviceContainer.get<ILanguageServerFolderService>(ILanguageServerFolderService); - return lsFolderService.getLatestLanguageServerVersion().then(item => item!); - } - - public async downloadLanguageServer(context: IExtensionContext): Promise<void> { - const downloadInfo = await this.getDownloadInfo(); - const downloadUri = downloadInfo.uri; - const lsVersion = downloadInfo.version.raw; - 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('download failed.'); - this.output.appendLine(err); - success = false; - throw new Error(err); - } finally { - sendTelemetryEvent( - PYTHON_LANGUAGE_SERVER_DOWNLOADED, - timer.elapsedTime, - { success, lsVersion } - ); - } - - timer.reset(); - try { - await this.unpackArchive(context.extensionPath, localTempFilePath); - } catch (err) { - this.output.appendLine('extraction failed.'); - this.output.appendLine(err); - success = false; - throw new Error(err); - } finally { - sendTelemetryEvent( - PYTHON_LANGUAGE_SERVER_EXTRACTED, - timer.elapsedTime, - { success, lsVersion } - ); - await this.fs.deleteFile(localTempFilePath); - } - } - - private async downloadFile(uri: string, title: string): Promise<string> { - this.output.append(`Downloading ${uri}... `); - const tempFile = await this.fs.createTemporaryFile(downloadFileExtension); - - const deferred = createDeferred(); - const fileStream = this.fs.createWriteStream(tempFile.filePath); - fileStream.on('finish', () => { - fileStream.close(); - }).on('error', (err) => { - tempFile.dispose(); - deferred.reject(err); - }); - - await window.withProgress({ - location: ProgressLocation.Window - }, async (progress) => { - const httpClient = this.serviceContainer.get<IHttpClient>(IHttpClient); - const req = await httpClient.downloadFile(uri); - const requestProgress = await import('request-progress'); - requestProgress(req) - .on('progress', (state) => { - // https://www.npmjs.com/package/request-progress - const received = Math.round(state.size.transferred / 1024); - const total = Math.round(state.size.total / 1024); - const percentage = Math.round(100 * state.percent); - progress.report({ - message: `${title}${received} of ${total} KB (${percentage}%)` - }); - }) - .on('error', (err) => { - deferred.reject(err); - }) - .on('end', () => { - this.output.appendLine('complete.'); - deferred.resolve(); - }) - .pipe(fileStream); - return deferred.promise; - }); - - return tempFile.filePath; - } - - private async unpackArchive(extensionPath: string, tempFilePath: string): Promise<void> { - this.output.append('Unpacking archive... '); - - const installFolder = path.join(extensionPath, this.engineFolder); - 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(installFolder)) { - await this.fs.createDirectory(installFolder); - } - zip.extract(null, installFolder, (err) => { - 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 => { - deferred.reject(e); - }); - return deferred.promise; - }); - - // Set file to executable (nothing happens in Windows, as chmod has no definition there) - const executablePath = path.join(installFolder, this.platformData.getEngineExecutableName()); - await this.fs.chmod(executablePath, '0764'); // -rwxrw-r-- - - this.output.appendLine('done.'); - } -} diff --git a/src/client/activation/extensionSurvey.ts b/src/client/activation/extensionSurvey.ts new file mode 100644 index 000000000000..d32ba7180c0f --- /dev/null +++ b/src/client/activation/extensionSurvey.ts @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as querystring from 'querystring'; +import { env, UIKind } from 'vscode'; +import { IApplicationEnvironment, IApplicationShell, IWorkspaceService } from '../common/application/types'; +import { ShowExtensionSurveyPrompt } from '../common/experiments/groups'; +import '../common/extensions'; +import { IPlatformService } from '../common/platform/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'; + +// persistent state names, exported to make use of in testing +export enum extensionSurveyStateKeys { + doNotShowAgain = 'doNotShowExtensionSurveyAgain', + disableSurveyForTime = 'doNotShowExtensionSurveyUntilTime', +} + +const timeToDisableSurveyFor = 1000 * 60 * 60 * 24 * 7 * 12; // 12 weeks +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(IExperimentService) private experiments: IExperimentService, + @inject(IApplicationEnvironment) private appEnvironment: IApplicationEnvironment, + @inject(IPlatformService) private platformService: IPlatformService, + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + private sampleSizePerOneHundredUsers: number = 10, + private waitTimeToShowSurvey: number = WAIT_TIME_TO_SHOW_SURVEY, + ) {} + + public async activate(): Promise<void> { + if (!(await this.experiments.inExperiment(ShowExtensionSurveyPrompt.experiment))) { + return; + } + const show = this.shouldShowBanner(); + if (!show) { + return; + } + setTimeout(() => this.showSurvey().ignoreErrors(), this.waitTimeToShowSurvey); + } + + @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<boolean>('feedback.enabled', true); + } + + if (!feedbackEnabled) { + return false; + } + + const doNotShowSurveyAgain = this.persistentState.createGlobalPersistentState( + extensionSurveyStateKeys.doNotShowAgain, + false, + ); + if (doNotShowSurveyAgain.value) { + return false; + } + const isSurveyDisabledForTimeState = this.persistentState.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + timeToDisableSurveyFor, + ); + if (isSurveyDisabledForTimeState.value) { + return false; + } + // we only want 10% of folks to see this survey. + const randomSample: number = this.random.getRandomInt(0, 100); + if (randomSample >= this.sampleSizePerOneHundredUsers) { + return false; + } + return true; + } + + @traceDecoratorError('Failed to display prompt for extension survey') + public async showSurvey() { + const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain]; + const telemetrySelections: ['Yes', 'Maybe later', "Don't show again"] = [ + 'Yes', + 'Maybe later', + "Don't show again", + ]; + const selection = await this.appShell.showInformationMessage(ExtensionSurveyBanner.bannerMessage, ...prompts); + sendTelemetryEvent(EventName.EXTENSION_SURVEY_PROMPT, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, + }); + if (!selection) { + return; + } + if (selection === ExtensionSurveyBanner.bannerLabelYes) { + this.launchSurvey(); + // Disable survey for a few weeks + await this.persistentState + .createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + timeToDisableSurveyFor, + ) + .updateValue(true); + } else if (selection === Common.doNotShowAgain) { + // Never show the survey again + await this.persistentState + .createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false) + .updateValue(true); + } + } + + private launchSurvey() { + const query = querystring.stringify({ + o: encodeURIComponent(this.platformService.osType), // platform + v: encodeURIComponent(this.appEnvironment.vscodeVersion), + e: encodeURIComponent(this.appEnvironment.packageJson.version), // extension version + m: encodeURIComponent(this.appEnvironment.sessionId), + }); + const url = `https://aka.ms/AA5rjx5?${query}`; + this.browserService.launch(url); + } +} diff --git a/src/client/activation/hashVerifier.ts b/src/client/activation/hashVerifier.ts deleted file mode 100644 index e0b405ff5877..000000000000 --- a/src/client/activation/hashVerifier.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { createHash } from 'crypto'; -import * as fs from 'fs'; -import { createDeferred } from '../common/utils/async'; - -export class HashVerifier { - public async verifyHash(filePath: string, platformString: string, expectedDigest: string): Promise<boolean> { - const readStream = fs.createReadStream(filePath); - const deferred = createDeferred(); - const hash = createHash('sha512'); - hash.setEncoding('hex'); - readStream - .on('end', () => { - hash.end(); - deferred.resolve(); - }) - .on('error', (err) => { - deferred.reject(`Unable to calculate file hash. Error ${err}`); - }); - - readStream.pipe(hash); - await deferred.promise; - const actual = hash.read() as string; - return expectedDigest === platformString ? true : actual.toLowerCase() === expectedDigest.toLowerCase(); - } -} diff --git a/src/client/activation/interpreterDataService.ts b/src/client/activation/interpreterDataService.ts deleted file mode 100644 index 1e082bb171d5..000000000000 --- a/src/client/activation/interpreterDataService.ts +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { createHash } from 'crypto'; -import * as fs from 'fs'; -import * as path from 'path'; -import { ExtensionContext, Uri } from 'vscode'; -import { IApplicationShell } from '../common/application/types'; -import '../common/extensions'; -import { IPlatformService } from '../common/platform/types'; -import { IPythonExecutionFactory, IPythonExecutionService } from '../common/process/types'; -import { createDeferred } from '../common/utils/async'; -import { IServiceContainer } from '../ioc/types'; - -const DataVersion = 1; - -export class InterpreterData { - constructor( - public readonly dataVersion: number, - // tslint:disable-next-line:no-shadowed-variable - public readonly path: string, - public readonly version: string, - public readonly searchPaths: string, - public readonly hash: string - ) { } -} - -export class InterpreterDataService { - constructor( - private readonly context: ExtensionContext, - private readonly serviceContainer: IServiceContainer) { } - - public async getInterpreterData(resource?: Uri): Promise<InterpreterData | undefined> { - const executionFactory = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); - const execService = await executionFactory.create({ resource }); - - const interpreterPath = await execService.getExecutablePath(); - if (interpreterPath.length === 0) { - return; - } - - const cacheKey = `InterpreterData-${interpreterPath}`; - let interpreterData = this.context.globalState.get<InterpreterData>(cacheKey); - let interpreterChanged = false; - if (interpreterData) { - // Check if interpreter executable changed - if (interpreterData.dataVersion !== DataVersion) { - interpreterChanged = true; - } else { - const currentHash = await this.getInterpreterHash(interpreterPath); - interpreterChanged = currentHash !== interpreterData.hash; - } - } - - if (interpreterChanged || !interpreterData) { - interpreterData = await this.getInterpreterDataFromPython(execService, interpreterPath); - this.context.globalState.update(interpreterPath, interpreterData); - } else { - // Make sure we verify that search paths did not change. This must be done - // completely async so we don't delay Python language server startup. - this.verifySearchPaths(interpreterData.searchPaths, interpreterPath, execService); - } - return interpreterData; - } - - public getInterpreterHash(interpreterPath: string): Promise<string> { - const platform = this.serviceContainer.get<IPlatformService>(IPlatformService); - const pythonExecutable = path.join(path.dirname(interpreterPath), platform.isWindows ? 'python.exe' : 'python'); - // Hash mod time and creation time - const deferred = createDeferred<string>(); - fs.lstat(pythonExecutable, (err, stats) => { - if (err) { - deferred.resolve(''); - } else { - const actual = createHash('sha512').update(`${stats.ctime}-${stats.mtime}`).digest('hex'); - deferred.resolve(actual); - } - }); - return deferred.promise; - } - - private async getInterpreterDataFromPython(execService: IPythonExecutionService, interpreterPath: string): Promise<InterpreterData> { - const result = await execService.exec(['-c', 'import sys; print(sys.version_info)'], {}); - // sys.version_info(major=3, minor=6, micro=6, releaselevel='final', serial=0) - if (!result.stdout) { - throw Error('Unable to determine Python interpreter version and system prefix.'); - } - const output = result.stdout.splitLines({ removeEmptyEntries: true, trim: true }); - if (!output || output.length < 1) { - throw Error('Unable to parse version and and system prefix from the Python interpreter output.'); - } - const majorMatches = output[0].match(/major=(\d*?),/); - const minorMatches = output[0].match(/minor=(\d*?),/); - if (!majorMatches || majorMatches.length < 2 || !minorMatches || minorMatches.length < 2) { - throw Error('Unable to parse interpreter version.'); - } - const hash = await this.getInterpreterHash(interpreterPath); - const searchPaths = await this.getSearchPaths(execService); - return new InterpreterData(DataVersion, interpreterPath, `${majorMatches[1]}.${minorMatches[1]}`, searchPaths, hash); - } - - private async getSearchPaths(execService: IPythonExecutionService): Promise<string> { - const result = await execService.exec(['-c', 'import sys; import os; print(sys.path + os.getenv("PYTHONPATH", "").split(os.pathsep));'], {}); - if (!result.stdout) { - throw Error('Unable to determine Python interpreter search paths.'); - } - // tslint:disable-next-line:no-unnecessary-local-variable - const paths = result.stdout.split(',') - .filter(p => this.isValidPath(p)) - .map(p => this.pathCleanup(p)); - return paths.join(';'); // PTVS uses ; on all platforms - } - - private pathCleanup(s: string): string { - s = s.trim(); - if (s[0] === '\'') { - s = s.substr(1); - } - if (s[s.length - 1] === ']') { - s = s.substr(0, s.length - 1); - } - if (s[s.length - 1] === '\'') { - s = s.substr(0, s.length - 1); - } - return s; - } - - private isValidPath(s: string): boolean { - return s.length > 0 && s[0] !== '['; - } - - private verifySearchPaths(currentPaths: string, interpreterPath: string, execService: IPythonExecutionService): void { - this.getSearchPaths(execService) - .then(async paths => { - if (paths !== currentPaths) { - this.context.globalState.update(interpreterPath, undefined); - const appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell); - await appShell.showWarningMessage('Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly.'); - } - }).ignoreErrors(); - } -} diff --git a/src/client/activation/jedi.ts b/src/client/activation/jedi.ts deleted file mode 100644 index f12debb4b0c4..000000000000 --- a/src/client/activation/jedi.ts +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { DocumentFilter, languages } from 'vscode'; -import { PYTHON } from '../common/constants'; -import { IConfigurationService, IExtensionContext, ILogger } 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 { activateGoToObjectDefinitionProvider } from '../providers/objectDefinitionProvider'; -import { PythonReferenceProvider } from '../providers/referenceProvider'; -import { PythonRenameProvider } from '../providers/renameProvider'; -import { PythonSignatureProvider } from '../providers/signatureProvider'; -import { JediSymbolProvider } from '../providers/symbolProvider'; -import { BlockFormatProviders } from '../typeFormatters/blockFormatProvider'; -import { OnTypeFormattingDispatcher } from '../typeFormatters/dispatcher'; -import { OnEnterFormatter } from '../typeFormatters/onEnterFormatter'; -import { IUnitTestManagementService } from '../unittests/types'; -import { WorkspaceSymbols } from '../workspaceSymbols/main'; -import { IExtensionActivator } from './types'; - -@injectable() -export class JediExtensionActivator implements IExtensionActivator { - private readonly context: IExtensionContext; - private jediFactory?: JediFactory; - private readonly documentSelector: DocumentFilter[]; - constructor(@inject(IServiceManager) private serviceManager: IServiceManager) { - this.context = this.serviceManager.get<IExtensionContext>(IExtensionContext); - this.documentSelector = PYTHON; - } - - public async activate(): Promise<boolean> { - const context = this.context; - - const jediFactory = this.jediFactory = new JediFactory(context.asAbsolutePath('.'), this.serviceManager); - context.subscriptions.push(jediFactory); - context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory)); - - context.subscriptions.push(jediFactory); - context.subscriptions.push(languages.registerRenameProvider(this.documentSelector, new PythonRenameProvider(this.serviceManager))); - const definitionProvider = new PythonDefinitionProvider(jediFactory); - - context.subscriptions.push(languages.registerDefinitionProvider(this.documentSelector, definitionProvider)); - context.subscriptions.push(languages.registerHoverProvider(this.documentSelector, new PythonHoverProvider(jediFactory))); - context.subscriptions.push(languages.registerReferenceProvider(this.documentSelector, new PythonReferenceProvider(jediFactory))); - context.subscriptions.push(languages.registerCompletionItemProvider(this.documentSelector, new PythonCompletionItemProvider(jediFactory, this.serviceManager), '.')); - context.subscriptions.push(languages.registerCodeLensProvider(this.documentSelector, this.serviceManager.get<IShebangCodeLensProvider>(IShebangCodeLensProvider))); - - const onTypeDispatcher = new OnTypeFormattingDispatcher({ - '\n': new OnEnterFormatter(), - ':': new BlockFormatProviders() - }); - const onTypeTriggers = onTypeDispatcher.getTriggerCharacters(); - if (onTypeTriggers) { - context.subscriptions.push(languages.registerOnTypeFormattingEditProvider(PYTHON, onTypeDispatcher, onTypeTriggers.first, ...onTypeTriggers.more)); - } - - const serviceContainer = this.serviceManager.get<IServiceContainer>(IServiceContainer); - context.subscriptions.push(new WorkspaceSymbols(serviceContainer)); - - const symbolProvider = new JediSymbolProvider(serviceContainer, jediFactory); - context.subscriptions.push(languages.registerDocumentSymbolProvider(this.documentSelector, symbolProvider)); - - const pythonSettings = this.serviceManager.get<IConfigurationService>(IConfigurationService).getSettings(); - if (pythonSettings.devOptions.indexOf('DISABLE_SIGNATURE') === -1) { - context.subscriptions.push(languages.registerSignatureHelpProvider(this.documentSelector, new PythonSignatureProvider(jediFactory), '(', ',')); - } - - context.subscriptions.push(languages.registerRenameProvider(PYTHON, new PythonRenameProvider(serviceContainer))); - - const testManagementService = this.serviceManager.get<IUnitTestManagementService>(IUnitTestManagementService); - testManagementService.activate() - .then(() => testManagementService.activateCodeLenses(symbolProvider)) - .catch(ex => this.serviceManager.get<ILogger>(ILogger).logError('Failed to activate Unit Tests', ex)); - - return true; - } - - public async deactivate(): Promise<void> { - 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<LanguageClient> { + // 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<void> { + this.lsVersion = + (options.middleware ? (<JediLanguageClientMiddleware>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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + if (this.languageServerProxy) { + await this.languageServerProxy.stop(); + } + } +} diff --git a/src/client/activation/languageClientMiddleware.ts b/src/client/activation/languageClientMiddleware.ts new file mode 100644 index 000000000000..d3d1e0c3c171 --- /dev/null +++ b/src/client/activation/languageClientMiddleware.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IServiceContainer } from '../ioc/types'; +import { sendTelemetryEvent } from '../telemetry'; + +import { LanguageClientMiddlewareBase } from './languageClientMiddlewareBase'; +import { LanguageServerType } from './types'; + +export class LanguageClientMiddleware extends LanguageClientMiddlewareBase { + public constructor(serviceContainer: IServiceContainer, serverType: LanguageServerType, serverVersion?: string) { + super(serviceContainer, serverType, sendTelemetryEvent, serverVersion); + } +} 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> = T extends PromiseLike<infer U> ? U : T; +type MiddleWareMethods = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [P in keyof Middleware]-?: NonNullable<Middleware[P]> 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<string, number> | number, properties?: any, ex?: Error): void; +} + +export class LanguageClientMiddlewareBase implements Middleware { + private readonly eventName: EventName | undefined; + + private readonly lastCaptured = new Map<string, number>(); + + 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>(IInterpreterService); + const envService = this.serviceContainer.get<IEnvironmentVariablesProvider>(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<boolean> { + return this.connectedPromise.promise; + } + + protected notebookAddon: (Middleware & Disposable) | undefined; + + private connectedPromise = createDeferred<boolean>(); + + 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<boolean>(); + 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<T extends keyof MiddleWareMethods>( + lspMethod: string, + debounceMilliseconds: number, + funcName: T, + args: IArguments, + lazyMeasures?: (this_: any, result: Awaited<ReturnType<MiddleWareMethods[T]>>) => Record<string, number>, + ): ReturnType<MiddleWareMethods[T]> { + 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<ReturnType<MiddleWareMethods[T]>>) => { + // 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<string, number> = 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<MiddleWareMethods[T]> = 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<MiddleWareMethods[T]>; + } +} diff --git a/src/client/activation/languageServer/languageServer.ts b/src/client/activation/languageServer/languageServer.ts deleted file mode 100644 index 9dc1d119ffb3..000000000000 --- a/src/client/activation/languageServer/languageServer.ts +++ /dev/null @@ -1,381 +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, CompletionContext, OutputChannel, Position, - TextDocument, Uri -} from 'vscode'; -import { - Disposable, LanguageClient, LanguageClientOptions, - ProvideCompletionItemsSignature, ServerOptions -} from 'vscode-languageclient'; -import { - IApplicationShell, ICommandManager, IWorkspaceService -} from '../../common/application/types'; -import { PythonSettings } from '../../common/configSettings'; -// tslint:disable-next-line:ordered-imports -import { isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; -import { Logger } from '../../common/logger'; -import { IFileSystem, IPlatformService } from '../../common/platform/types'; -import { - BANNER_NAME_LS_SURVEY, DeprecatedFeatureInfo, IConfigurationService, - IDisposableRegistry, IExtensionContext, IFeatureDeprecationManager, ILogger, - IOutputChannel, IPathUtils, IPythonExtensionBanner, IPythonSettings -} from '../../common/types'; -import { createDeferred, Deferred } from '../../common/utils/async'; -import { StopWatch } from '../../common/utils/stopWatch'; -import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { IServiceContainer } from '../../ioc/types'; -import { LanguageServerSymbolProvider } from '../../providers/symbolProvider'; -import { sendTelemetryEvent } from '../../telemetry'; -import { - PYTHON_LANGUAGE_SERVER_ENABLED, - PYTHON_LANGUAGE_SERVER_ERROR, - PYTHON_LANGUAGE_SERVER_TELEMETRY -} from '../../telemetry/constants'; -import { IUnitTestManagementService } from '../../unittests/types'; -import { LanguageServerDownloader } from '../downloader'; -import { InterpreterData, InterpreterDataService } from '../interpreterDataService'; -import { PlatformData } from '../platformData'; -import { ProgressReporting } from '../progress'; -import { IExtensionActivator, ILanguageServerFolderService } from '../types'; - -const PYTHON = 'python'; -const dotNetCommand = 'dotnet'; -const languageClientName = 'Python Tools'; -const loadExtensionCommand = 'python._loadLanguageServerExtension'; -const buildSymbolsCmdDeprecatedInfo: DeprecatedFeatureInfo = { - doNotDisplayPromptStateKey: 'SHOW_DEPRECATED_FEATURE_PROMPT_BUILD_WORKSPACE_SYMBOLS', - message: 'The command \'Python: Build Workspace Symbols\' is deprecated when using the Python Language Server. The Python Language Server builds symbols in the workspace in the background.', - moreInfoUrl: 'https://github.com/Microsoft/vscode-python/issues/2267#issuecomment-408996859', - commands: ['python.buildWorkspaceSymbols'] -}; - -@injectable() -export class LanguageServerExtensionActivator implements IExtensionActivator { - private readonly configuration: IConfigurationService; - private readonly appShell: IApplicationShell; - private readonly output: OutputChannel; - private readonly fs: IFileSystem; - private readonly sw = new StopWatch(); - private readonly platformData: PlatformData; - private readonly startupCompleted: Deferred<void>; - private readonly disposables: Disposable[] = []; - private readonly context: IExtensionContext; - private readonly workspace: IWorkspaceService; - private readonly root: Uri | undefined; - - private languageClient: LanguageClient | undefined; - private interpreterHash: string = ''; - private excludedFiles: string[] = []; - private typeshedPaths: string[] = []; - private loadExtensionArgs: {} | undefined; - private surveyBanner: IPythonExtensionBanner; - private languageServerFolder!: string; - private languageServerFolderService: ILanguageServerFolderService; - - constructor(@inject(IServiceContainer) private readonly services: IServiceContainer) { - this.context = this.services.get<IExtensionContext>(IExtensionContext); - this.configuration = this.services.get<IConfigurationService>(IConfigurationService); - this.appShell = this.services.get<IApplicationShell>(IApplicationShell); - this.output = this.services.get<OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.fs = this.services.get<IFileSystem>(IFileSystem); - this.platformData = new PlatformData(services.get<IPlatformService>(IPlatformService), this.fs); - this.workspace = this.services.get<IWorkspaceService>(IWorkspaceService); - this.languageServerFolderService = this.services.get<ILanguageServerFolderService>(ILanguageServerFolderService); - const deprecationManager: IFeatureDeprecationManager = - this.services.get<IFeatureDeprecationManager>(IFeatureDeprecationManager); - - // Currently only a single root. Multi-root support is future. - this.root = this.workspace && this.workspace.hasWorkspaceFolders - ? this.workspace.workspaceFolders![0]!.uri : undefined; - - this.startupCompleted = createDeferred<void>(); - const commandManager = this.services.get<ICommandManager>(ICommandManager); - - this.disposables.push(commandManager.registerCommand(loadExtensionCommand, - async (args) => { - if (this.languageClient) { - await this.startupCompleted.promise; - this.languageClient.sendRequest('python/loadExtension', args); - } else { - this.loadExtensionArgs = args; - } - } - )); - - deprecationManager.registerDeprecation(buildSymbolsCmdDeprecatedInfo); - - this.surveyBanner = services.get<IPythonExtensionBanner>(IPythonExtensionBanner, BANNER_NAME_LS_SURVEY); - - (this.configuration.getSettings() as PythonSettings).addListener('change', this.onSettingsChanged.bind(this)); - } - - public async activate(): Promise<boolean> { - this.sw.reset(); - this.languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(); - const clientOptions = await this.getAnalysisOptions(); - if (!clientOptions) { - return false; - } - - this.startupCompleted.promise.then(() => { - const testManagementService = this.services.get<IUnitTestManagementService>(IUnitTestManagementService); - testManagementService.activate() - .then(() => testManagementService.activateCodeLenses(new LanguageServerSymbolProvider(this.languageClient!))) - .catch(ex => this.services.get<ILogger>(ILogger).logError('Failed to activate Unit Tests', ex)); - }).ignoreErrors(); - - return this.startLanguageServer(clientOptions); - } - - public async deactivate(): Promise<void> { - if (this.languageClient) { - // Do not await on this - this.languageClient.stop(); - } - for (const d of this.disposables) { - d.dispose(); - } - (this.configuration.getSettings() as PythonSettings).removeListener('change', this.onSettingsChanged.bind(this)); - } - - private async startLanguageServer(clientOptions: LanguageClientOptions): Promise<boolean> { - // Determine if we are running MSIL/Universal via dotnet or self-contained app. - sendTelemetryEvent(PYTHON_LANGUAGE_SERVER_ENABLED); - - const settings = this.configuration.getSettings(); - if (!settings.downloadLanguageServer) { - // Depends on .NET Runtime or SDK. Typically development-only case. - this.languageClient = await this.createSimpleLanguageClient(clientOptions); - await this.startLanguageClient(); - return true; - } - - const mscorlib = path.join(this.context.extensionPath, this.languageServerFolder, 'mscorlib.dll'); - if (!await this.fs.fileExists(mscorlib)) { - const downloader = new LanguageServerDownloader(this.platformData, this.languageServerFolder, this.services); - await downloader.downloadLanguageServer(this.context); - } - - const serverModule = path.join(this.context.extensionPath, this.languageServerFolder, this.platformData.getEngineExecutableName()); - this.languageClient = await this.createSelfContainedLanguageClient(serverModule, clientOptions); - try { - await this.startLanguageClient(); - this.languageClient.onTelemetry(telemetryEvent => { - const eventName = telemetryEvent.EventName ? telemetryEvent.EventName : PYTHON_LANGUAGE_SERVER_TELEMETRY; - sendTelemetryEvent(eventName, telemetryEvent.Measurements, telemetryEvent.Properties); - }); - return true; - } catch (ex) { - this.appShell.showErrorMessage(`Language server failed to start. Error ${ex}`); - sendTelemetryEvent(PYTHON_LANGUAGE_SERVER_ERROR, undefined, { error: 'Failed to start (platform)' }); - return false; - } - } - - private async startLanguageClient(): Promise<void> { - this.context.subscriptions.push(this.languageClient!.start()); - await this.serverReady(); - const disposables = this.services.get<Disposable[]>(IDisposableRegistry); - const progressReporting = new ProgressReporting(this.languageClient!); - disposables.push(progressReporting); - } - - private async serverReady(): Promise<void> { - while (!this.languageClient!.initializeResult) { - await new Promise(resolve => setTimeout(resolve, 100)); - } - if (this.loadExtensionArgs) { - this.languageClient!.sendRequest('python/loadExtension', this.loadExtensionArgs); - } - - this.startupCompleted.resolve(); - } - - private async createSimpleLanguageClient(clientOptions: LanguageClientOptions): Promise<LanguageClient> { - const commandOptions = { stdio: 'pipe' }; - const serverModule = path.join(this.context.extensionPath, this.languageServerFolder, this.platformData.getEngineDllName()); - const serverOptions: ServerOptions = { - run: { command: dotNetCommand, args: [serverModule], options: commandOptions }, - debug: { command: dotNetCommand, args: [serverModule, '--debug'], options: commandOptions } - }; - const vscodeLanaguageClient = await import('vscode-languageclient'); - return new vscodeLanaguageClient.LanguageClient(PYTHON, languageClientName, serverOptions, clientOptions); - } - - private async createSelfContainedLanguageClient(serverModule: string, clientOptions: LanguageClientOptions): Promise<LanguageClient> { - const options = { stdio: 'pipe' }; - const serverOptions: ServerOptions = { - run: { command: serverModule, rgs: [], options: options }, - debug: { command: serverModule, args: ['--debug'], options } - }; - const vscodeLanaguageClient = await import('vscode-languageclient'); - return new vscodeLanaguageClient.LanguageClient(PYTHON, languageClientName, serverOptions, clientOptions); - } - - // tslint:disable-next-line:member-ordering - public async getAnalysisOptions(): Promise<LanguageClientOptions | undefined> { - const properties = new Map<string, {}>(); - let interpreterData: InterpreterData | undefined; - let pythonPath = ''; - - try { - const interpreterDataService = new InterpreterDataService(this.context, this.services); - interpreterData = await interpreterDataService.getInterpreterData(); - } catch (ex) { - Logger.error('Unable to determine path to the Python interpreter. IntelliSense will be limited.', ex); - } - - this.interpreterHash = interpreterData ? interpreterData.hash : ''; - if (interpreterData) { - pythonPath = path.dirname(interpreterData.path); - // tslint:disable-next-line:no-string-literal - properties['InterpreterPath'] = interpreterData.path; - // tslint:disable-next-line:no-string-literal - properties['Version'] = interpreterData.version; - } - - // tslint:disable-next-line:no-string-literal - properties['DatabasePath'] = path.join(this.context.extensionPath, this.languageServerFolder); - - let searchPaths = interpreterData ? interpreterData.searchPaths.split(path.delimiter) : []; - const settings = this.configuration.getSettings(); - if (settings.autoComplete) { - const extraPaths = settings.autoComplete.extraPaths; - if (extraPaths && extraPaths.length > 0) { - searchPaths.push(...extraPaths); - } - } - const envVarsProvider = this.services.get<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider); - const vars = await envVarsProvider.getEnvironmentVariables(); - if (vars.PYTHONPATH && vars.PYTHONPATH.length > 0) { - const pathUtils = this.services.get<IPathUtils>(IPathUtils); - const paths = vars.PYTHONPATH.split(pathUtils.delimiter).filter(item => item.trim().length > 0); - searchPaths.push(...paths); - } - // Make sure paths do not contain multiple slashes so file URIs - // in VS Code (Node.js) and in the language server (.NET) match. - // Note: for the language server paths separator is always ; - searchPaths.push(pythonPath); - searchPaths = searchPaths.map(p => path.normalize(p)); - - const selector = [{ language: PYTHON, scheme: 'file' }]; - this.excludedFiles = this.getExcludedFiles(); - this.typeshedPaths = this.getTypeshedPaths(settings); - - // Options to control the language client - return { - // Register the server for Python documents - documentSelector: selector, - synchronize: { - configurationSection: PYTHON - }, - outputChannel: this.output, - initializationOptions: { - interpreter: { - properties - }, - displayOptions: { - preferredFormat: 'markdown', - trimDocumentationLines: false, - maxDocumentationLineLength: 0, - trimDocumentationText: false, - maxDocumentationTextLength: 0 - }, - searchPaths, - typeStubSearchPaths: this.typeshedPaths, - excludeFiles: this.excludedFiles, - testEnvironment: isTestExecution(), - analysisUpdates: true, - traceLogging: true, // Max level, let LS decide through settings actual level of logging. - asyncStartup: true - }, - middleware: { - provideCompletionItem: (document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature) => { - if (this.surveyBanner) { - this.surveyBanner.showBanner().ignoreErrors(); - } - return next(document, position, context, token); - } - } - }; - } - - private 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; - } - - private getVsCodeExcludeSection(setting: string, list: string[]): void { - const states = this.workspace.getConfiguration(setting, this.root); - if (states) { - Object.keys(states) - .filter(k => (k.indexOf('*') >= 0 || k.indexOf('/') >= 0) && states[k]) - .forEach(p => list.push(p)); - } - } - - private getPythonExcludeSection(list: string[]): void { - const pythonSettings = this.configuration.getSettings(this.root); - 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)); - } - } - - private getTypeshedPaths(settings: IPythonSettings): string[] { - return settings.analysis.typeshedPaths && settings.analysis.typeshedPaths.length > 0 - ? settings.analysis.typeshedPaths - : [path.join(this.context.extensionPath, this.languageServerFolder, 'Typeshed')]; - } - - private async onSettingsChanged(): Promise<void> { - const ids = new InterpreterDataService(this.context, this.services); - const idata = await ids.getInterpreterData(); - if (!idata || idata.hash !== this.interpreterHash) { - this.interpreterHash = idata ? idata.hash : ''; - await this.restartLanguageServer(); - return; - } - - const excludedFiles = this.getExcludedFiles(); - await this.restartLanguageServerIfArrayChanged(this.excludedFiles, excludedFiles); - - const settings = this.configuration.getSettings(); - const typeshedPaths = this.getTypeshedPaths(settings); - await this.restartLanguageServerIfArrayChanged(this.typeshedPaths, typeshedPaths); - } - - private async restartLanguageServerIfArrayChanged(oldArray: string[], newArray: string[]): Promise<void> { - if (newArray.length !== oldArray.length) { - await this.restartLanguageServer(); - return; - } - - for (let i = 0; i < oldArray.length; i += 1) { - if (oldArray[i] !== newArray[i]) { - await this.restartLanguageServer(); - return; - } - } - } - - private async restartLanguageServer(): Promise<void> { - if (!this.context) { - return; - } - await this.deactivate(); - await this.activate(); - } -} diff --git a/src/client/activation/languageServer/languageServerCompatibilityService.ts b/src/client/activation/languageServer/languageServerCompatibilityService.ts deleted file mode 100644 index 97e50b664426..000000000000 --- a/src/client/activation/languageServer/languageServerCompatibilityService.ts +++ /dev/null @@ -1,27 +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 { PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED } from '../../telemetry/constants'; -import { ILanguageServerCompatibilityService } from '../types'; - -@injectable() -export class LanguageServerCompatibilityService implements ILanguageServerCompatibilityService { - constructor(@inject(IDotNetCompatibilityService) private readonly dotnetCompatibility: IDotNetCompatibilityService) { } - public async isSupported(): Promise<boolean> { - try { - const supported = await this.dotnetCompatibility.isSupported(); - sendTelemetryEvent(PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED, undefined, { supported }); - return supported; - } catch (ex) { - traceError('Unable to determine whether LS is supported', ex); - sendTelemetryEvent(PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED, undefined, { supported: false, failureType: 'UnknownError' }); - return false; - } - } -} diff --git a/src/client/activation/languageServer/languageServerFolderService.ts b/src/client/activation/languageServer/languageServerFolderService.ts deleted file mode 100644 index 31d705d76ae7..000000000000 --- a/src/client/activation/languageServer/languageServerFolderService.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 * 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 } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { FolderVersionPair, IDownloadChannelRule, ILanguageServerFolderService, ILanguageServerPackageService } from '../types'; - -const languageServerFolder = 'languageServer'; - -@injectable() -export class LanguageServerFolderService implements ILanguageServerFolderService { - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { } - - @traceDecorators.verbose('Get language server folder name') - public async getLanguageServerFolderName(): Promise<string> { - const currentFolder = await this.getCurrentLanguageServerDirectory(); - let serverVersion: NugetPackage | undefined; - - const shouldLookForNewVersion = await this.shouldLookForNewLanguageServer(currentFolder); - if (currentFolder && !shouldLookForNewVersion) { - return path.basename(currentFolder.path); - } - - serverVersion = await this.getLatestLanguageServerVersion() - .catch(ex => undefined); - - if (currentFolder && (!serverVersion || serverVersion.version.compare(currentFolder.version) <= 0)) { - return path.basename(currentFolder.path); - } - - return `${languageServerFolder}.${serverVersion!.version.raw}`; - } - - @traceDecorators.verbose('Get latest version of Language Server') - public getLatestLanguageServerVersion(): Promise<NugetPackage | undefined> { - const lsPackageService = this.serviceContainer.get<ILanguageServerPackageService>(ILanguageServerPackageService); - return lsPackageService.getLatestNugetPackageVersion(); - } - public async shouldLookForNewLanguageServer(currentFolder?: FolderVersionPair): Promise<boolean> { - const configService = this.serviceContainer.get<IConfigurationService>(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>(IDownloadChannelRule, downloadChannel); - return rule.shouldLookForNewLanguageServer(currentFolder); - } - public async getCurrentLanguageServerDirectory(): Promise<FolderVersionPair | undefined> { - const configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); - if (!configService.getSettings().downloadLanguageServer) { - return { path: 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<FolderVersionPair[]> { - const fs = this.serviceContainer.get<IFileSystem>(IFileSystem); - const subDirs = await fs.getSubDirectories(EXTENSION_ROOT_DIR); - return subDirs - .filter(dir => path.basename(dir).startsWith(languageServerFolder)) - .map(dir => { return { path: dir, version: this.getFolderVersion(path.basename(dir)) }; }); - } - - public getFolderVersion(dirName: string): semver.SemVer { - const suffix = dirName.substring(languageServerFolder.length + 1); - return suffix.length === 0 ? new semver.SemVer('0.0.0') : (semver.parse(suffix, true) || new semver.SemVer('0.0.0')); - } - private getDownloadChannel() { - const lsPackageService = this.serviceContainer.get<ILanguageServerPackageService>(ILanguageServerPackageService); - return lsPackageService.getLanguageServerDownloadChannel(); - } -} diff --git a/src/client/activation/languageServer/languageServerHashes.ts b/src/client/activation/languageServer/languageServerHashes.ts deleted file mode 100644 index d8d856064363..000000000000 --- a/src/client/activation/languageServer/languageServerHashes.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// This file will be replaced by a generated one during the release build -// with actual hashes of the uploaded packages. -// Values are for test purposes only -export const language_server_win_x86_sha512 = 'win-x86'; -export const language_server_win_x64_sha512 = 'win-x64'; -export const language_server_osx_x64_sha512 = 'osx-x64'; -export const language_server_linux_x64_sha512 = 'linux-x64'; diff --git a/src/client/activation/languageServer/languageServerPackageRepository.ts b/src/client/activation/languageServer/languageServerPackageRepository.ts deleted file mode 100644 index e6f1f063aff4..000000000000 --- a/src/client/activation/languageServer/languageServerPackageRepository.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 { AzureBlobStoreNugetRepository } from '../../common/nuget/azureBlobStoreNugetRepository'; -import { IServiceContainer } from '../../ioc/types'; - -const azureBlobStorageAccount = 'https://pvsc.blob.core.windows.net'; -export const azureCDNBlobStorageAccount = 'https://pvsc.azureedge.net'; - -export enum LanguageServerDownloadChannel { - stable = 'stable', - beta = 'beta', - daily = 'daily' -} - -export enum LanguageServerPackageStorageContainers { - stable = 'python-language-server-stable', - beta = 'python-language-server-beta', - daily = 'python-language-server-daily' -} - -@injectable() -export class StableLanguageServerPackageRepository extends AzureBlobStoreNugetRepository { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer, azureBlobStorageAccount, LanguageServerPackageStorageContainers.stable, azureCDNBlobStorageAccount); - } -} - -@injectable() -export class BetaLanguageServerPackageRepository extends AzureBlobStoreNugetRepository { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer, azureBlobStorageAccount, LanguageServerPackageStorageContainers.beta, azureCDNBlobStorageAccount); - } -} - -@injectable() -export class DailyLanguageServerPackageRepository extends AzureBlobStoreNugetRepository { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer, azureBlobStorageAccount, LanguageServerPackageStorageContainers.daily, azureCDNBlobStorageAccount); - } -} diff --git a/src/client/activation/languageServer/languageServerPackageService.ts b/src/client/activation/languageServer/languageServerPackageService.ts deleted file mode 100644 index 74d1e92bcd8e..000000000000 --- a/src/client/activation/languageServer/languageServerPackageService.ts +++ /dev/null @@ -1,98 +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 { 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 } from '../../common/types'; -import { OSType } from '../../common/utils/platform'; -import { IServiceContainer } from '../../ioc/types'; -import { PlatformName } from '../platformData'; -import { ILanguageServerPackageService } from '../types'; -import { azureCDNBlobStorageAccount, LanguageServerPackageStorageContainers } from './languageServerPackageRepository'; - -const downloadBaseFileName = 'Python-Language-Server'; -export const maxMajorVersion = 0; -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 LanguageServerPackageService implements ILanguageServerPackageService { - public maxMajorVersion: number = maxMajorVersion; - constructor(@inject(IServiceContainer) protected readonly serviceContainer: IServiceContainer, - @inject(IApplicationEnvironment) private readonly appEnv: IApplicationEnvironment, - @inject(IPlatformService) private readonly platform: IPlatformService) { } - 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]; - } - } - } - - @traceDecorators.verbose('Get latest language server nuget package version') - public async getLatestNugetPackageVersion(): Promise<NugetPackage> { - const downloadChannel = this.getLanguageServerDownloadChannel(); - const nugetRepo = this.serviceContainer.get<INugetRepository>(INugetRepository, downloadChannel); - const packageName = this.getNugetPackageName(); - traceVerbose(`Listing packages for ${downloadChannel} for ${packageName}`); - const packages = await nugetRepo.getPackages(packageName); - - return this.getValidPackage(packages); - } - - public getLanguageServerDownloadChannel(): LanguageServerDownloadChannels { - const configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); - const settings = configService.getSettings(); - if (settings.analysis.downloadChannel) { - return settings.analysis.downloadChannel; - } - - const isAlphaVersion = this.isAlphaVersionOfExtension(); - return isAlphaVersion ? 'beta' : 'stable'; - } - protected getValidPackage(packages: NugetPackage[]): NugetPackage { - const nugetService = this.serviceContainer.get<INugetService>(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]; - const minimumVersion = this.appEnv.packageJson.languageServerVersion as string; - 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: LanguageServerPackageStorageContainers.stable, - uri: `${azureCDNBlobStorageAccount}/${LanguageServerPackageStorageContainers.stable}/${this.getNugetPackageName()}.${minimumVersion}.nupkg` - }; - } - - private isAlphaVersionOfExtension() { - const extensions = this.serviceContainer.get<IExtensions>(IExtensions); - const extension = extensions.getExtension(PVSC_EXTENSION_ID)!; - const version = parse(extension.packageJSON.version)!; - return version.prerelease.length > 0 && version.prerelease[0] === 'alpha'; - } -} diff --git a/src/client/activation/node/analysisOptions.ts b/src/client/activation/node/analysisOptions.ts new file mode 100644 index 000000000000..71295649c25a --- /dev/null +++ b/src/client/activation/node/analysisOptions.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { LanguageClientOptions } from 'vscode-languageclient'; +import { IWorkspaceService } from '../../common/application/types'; + +import { LanguageServerAnalysisOptionsBase } from '../common/analysisOptions'; +import { ILanguageServerOutputChannel } from '../types'; + +export class NodeLanguageServerAnalysisOptions extends LanguageServerAnalysisOptionsBase { + // 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<LanguageClientOptions> { + 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 new file mode 100644 index 000000000000..9543f265468f --- /dev/null +++ b/src/client/activation/node/languageClientFactory.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node'; + +import { PYLANCE_EXTENSION_ID, PYTHON_LANGUAGE } from '../../common/constants'; +import { IFileSystem } from '../../common/platform/types'; +import { IExtensions, Resource } from '../../common/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { FileBasedCancellationStrategy } from '../common/cancellationUtils'; +import { ILanguageClientFactory } from '../types'; + +export const PYLANCE_NAME = 'Pylance'; + +export class NodeLanguageClientFactory implements ILanguageClientFactory { + constructor(private readonly fs: IFileSystem, private readonly extensions: IExtensions) {} + + public async createLanguageClient( + _resource: Resource, + _interpreter: PythonEnvironment | undefined, + clientOptions: LanguageClientOptions, + ): Promise<LanguageClient> { + // this must exist for node language client + const commandArgs = (clientOptions.connectionOptions + ?.cancellationStrategy as FileBasedCancellationStrategy).getCommandLineArguments(); + + 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'] }; + + // If the extension is launched in debug mode, then the debug server options are used. + const serverOptions: ServerOptions = { + run: { + module: bundlePath, + transport: TransportKind.ipc, + 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 + // someone starts the production extension in debug mode. + debug: { + module: modulePath, + transport: TransportKind.ipc, + options: debugOptions, + args: commandArgs, + }, + }; + + 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/languageServerProxy.ts b/src/client/activation/node/languageServerProxy.ts new file mode 100644 index 000000000000..45d1d1a17fee --- /dev/null +++ b/src/client/activation/node/languageServerProxy.ts @@ -0,0 +1,236 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import '../../common/extensions'; + +import { + DidChangeConfigurationNotification, + Disposable, + LanguageClient, + LanguageClientOptions, +} from 'vscode-languageclient/node'; + +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 { FileBasedCancellationStrategy } from '../common/cancellationUtils'; +import { ProgressReporting } from '../progress'; +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<T extends boolean | number | string> { + value: T | undefined; + } +} + +export class NodeLanguageServerProxy implements ILanguageServerProxy { + public languageClient: LanguageClient | undefined; + + private cancellationStrategy: FileBasedCancellationStrategy | undefined; + + private readonly disposables: Disposable[] = []; + + private lsVersion: string | undefined; + + private pylanceApi: PylanceApi | undefined; + + constructor( + 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, + }; + } + + @traceDecoratorVerbose('Disposing language server') + public dispose(): void { + this.stop().ignoreErrors(); + } + + @traceDecoratorError('Failed to start language server') + @captureTelemetry( + EventName.LANGUAGE_SERVER_ENABLED, + undefined, + true, + undefined, + NodeLanguageServerProxy.versionTelemetryProps, + ) + public async start( + resource: Resource, + interpreter: PythonEnvironment | undefined, + options: LanguageClientOptions, + ): Promise<void> { + const extension = await this.getPylanceExtension(); + this.lsVersion = extension?.packageJSON.version || '0'; + + 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<void> { + 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); + } + } + + if (this.cancellationStrategy) { + this.cancellationStrategy.dispose(); + this.cancellationStrategy = undefined; + } + } + + // eslint-disable-next-line class-methods-use-this + public loadExtension(): void { + // No body. + } + + @captureTelemetry( + EventName.LANGUAGE_SERVER_READY, + undefined, + true, + undefined, + NodeLanguageServerProxy.versionTelemetryProps, + ) + 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<InExperiment.IResponse> => { + const inExperiment = await this.experimentService.inExperiment(params.experimentName); + return { inExperiment }; + }, + ); + + client.onRequest( + GetExperimentValue.Method, + async <T extends boolean | number | string>( + params: GetExperimentValue.IRequest, + ): Promise<GetExperimentValue.IResponse<T>> => { + const value = await this.experimentService.getExperimentValue<T>(params.experimentName); + return { value }; + }, + ); + + this.disposables.push( + client.onRequest('python/isTrustedWorkspace', async () => ({ + isTrusted: this.workspace.isTrusted, + })), + ); + } + + private async getPylanceExtension(): Promise<Extension<PylanceApi> | undefined> { + const extension = this.extensions.getExtension<PylanceApi>(PYLANCE_EXTENSION_ID); + if (!extension) { + return undefined; + } + + if (!extension.isActive) { + await extension.activate(); + } + + return extension; + } +} diff --git a/src/client/activation/node/manager.ts b/src/client/activation/node/manager.ts new file mode 100644 index 000000000000..5a66e4abecd0 --- /dev/null +++ b/src/client/activation/node/manager.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import '../../common/extensions'; + +import { ICommandManager } from '../../common/application/types'; +import { IDisposable, IExtensions, Resource } from '../../common/types'; +import { debounceSync } from '../../common/utils/decorators'; +import { IServiceContainer } from '../../ioc/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { Commands } from '../commands'; +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 resource!: Resource; + + private interpreter: PythonEnvironment | undefined; + + private middleware: NodeLanguageClientMiddleware | undefined; + + private disposables: IDisposable[] = []; + + private connected = false; + + private lsVersion: string | undefined; + + private started = false; + + private static commandDispose: IDisposable; + + constructor( + private readonly serviceContainer: IServiceContainer, + private readonly analysisOptions: ILanguageServerAnalysisOptions, + private readonly languageServerProxy: NodeLanguageServerProxy, + commandManager: ICommandManager, + private readonly extensions: IExtensions, + ) { + 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, + }; + } + + public dispose(): void { + this.stopLanguageServer().ignoreErrors(); + NodeLanguageServerManager.commandDispose.dispose(); + this.disposables.forEach((d) => d.dispose()); + } + + @traceDecoratorError('Failed to start language server') + public async start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise<void> { + 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 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(): 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 { + sendTelemetryEvent(EventName.LANGUAGE_SERVER_RESTART, undefined, { reason: 'settings' }); + this.restartLanguageServer().ignoreErrors(); + } + + @traceDecoratorError('Failed to restart language server') + @traceDecoratorVerbose('Restarting language server') + protected async restartLanguageServer(): Promise<void> { + await this.stopLanguageServer(); + await this.startLanguageServer(); + } + + @captureTelemetry( + EventName.LANGUAGE_SERVER_STARTUP, + undefined, + true, + undefined, + NodeLanguageServerManager.versionTelemetryProps, + ) + @traceDecoratorVerbose('Starting language server') + protected async startLanguageServer(): Promise<void> { + 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) { + 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<void> { + 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<void>; + stop(): Promise<void>; + }; + notebook?: { + registerJupyterPythonPathFunction(func: (uri: Uri) => Promise<string | undefined>): void; + getCompletionItems( + document: TextDocument, + position: Position, + context: CompletionContext, + token: CancellationToken, + ): Promise<CompletionItem[] | CompletionList | undefined>; + }; +} 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<void> { + 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/platformData.ts b/src/client/activation/platformData.ts deleted file mode 100644 index fc0f01b2b2de..000000000000 --- a/src/client/activation/platformData.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IFileSystem, IPlatformService } from '../common/platform/types'; -import { - language_server_linux_x64_sha512, - language_server_osx_x64_sha512, - language_server_win_x64_sha512, - language_server_win_x86_sha512 -} from './languageServer/languageServerHashes'; - -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' -} - -export class PlatformData { - constructor(private platform: IPlatformService, fs: IFileSystem) { } - public getPlatformName(): 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 getEngineDllName(): string { - return 'Microsoft.Python.LanguageServer.dll'; - } - - public getEngineExecutableName(): 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'; - } - } - - public async getExpectedHash(): Promise<string> { - if (this.platform.isWindows) { - return this.platform.is64bit ? language_server_win_x64_sha512 : language_server_win_x86_sha512; - } - if (this.platform.isMac) { - return language_server_osx_x64_sha512; - } - if (this.platform.isLinux && this.platform.is64bit) { - return language_server_linux_x64_sha512; - } - throw new Error('Unknown platform.'); - } -} diff --git a/src/client/activation/progress.ts b/src/client/activation/progress.ts index e182d3be6404..5abcb9e553c0 100644 --- a/src/client/activation/progress.ts +++ b/src/client/activation/progress.ts @@ -1,89 +1,66 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +'use strict'; + import { Progress, ProgressLocation, window } from 'vscode'; -import { Disposable, LanguageClient } from 'vscode-languageclient'; +import { Disposable, LanguageClient } from 'vscode-languageclient/node'; import { createDeferred, Deferred } from '../common/utils/async'; -import { StopWatch } from '../common/utils/stopWatch'; -import { sendTelemetryEvent } from '../telemetry'; -import { PYTHON_LANGUAGE_SERVER_ANALYSISTIME } from '../telemetry/constants'; - -// Draw the line at Language Server analysis 'timing out' -// and becoming a failure-case at 1 minute: -const ANALYSIS_TIMEOUT_MS: number = 60000; export class ProgressReporting implements Disposable { - private statusBarMessage: Disposable | undefined; - private progress: Progress<{ message?: string; increment?: number }> | undefined; - private progressDeferred: Deferred<void> | undefined; - private progressTimer?: StopWatch; - // tslint:disable-next-line:no-unused-variable - private progressTimeout?: NodeJS.Timer; - - constructor(private readonly languageClient: LanguageClient) { - this.languageClient.onNotification('python/setStatusBarMessage', (m: string) => { - if (this.statusBarMessage) { - this.statusBarMessage.dispose(); - } - this.statusBarMessage = window.setStatusBarMessage(m); - }); - - this.languageClient.onNotification('python/beginProgress', async _ => { - if (this.progressDeferred) { - return; - } + private statusBarMessage: Disposable | undefined; + private progress: Progress<{ message?: string; increment?: number }> | undefined; + private progressDeferred: Deferred<void> | undefined; - this.progressDeferred = createDeferred<void>(); - this.progressTimer = new StopWatch(); - this.progressTimeout = setTimeout( - this.handleTimeout.bind(this), - ANALYSIS_TIMEOUT_MS - ); + constructor(private readonly languageClient: LanguageClient) { + this.languageClient.onNotification('python/setStatusBarMessage', (m: string) => { + if (this.statusBarMessage) { + this.statusBarMessage.dispose(); + } + this.statusBarMessage = window.setStatusBarMessage(m); + }); - window.withProgress({ - location: ProgressLocation.Window, - title: '' - }, progress => { - this.progress = progress; - return this.progressDeferred!.promise; - }); - }); + this.languageClient.onNotification('python/beginProgress', (_) => { + if (this.progressDeferred) { + return; + } + this.beginProgress(); + }); - this.languageClient.onNotification('python/reportProgress', (m: string) => { - if (!this.progress) { - return; - } - this.progress.report({ message: m }); - }); + this.languageClient.onNotification('python/reportProgress', (m: string) => { + if (!this.progress) { + this.beginProgress(); + } + this.progress!.report({ message: m }); // NOSONAR + }); - this.languageClient.onNotification('python/endProgress', _ => { - if (this.progressDeferred) { - this.progressDeferred.resolve(); - this.progressDeferred = undefined; - this.progress = undefined; - this.completeAnalysisTracking(true); - } - }); - } - public dispose() { - if (this.statusBarMessage) { - this.statusBarMessage.dispose(); + this.languageClient.onNotification('python/endProgress', (_) => { + if (this.progressDeferred) { + this.progressDeferred.resolve(); + this.progressDeferred = undefined; + this.progress = undefined; + } + }); } - } - private completeAnalysisTracking(success: boolean): void { - if (this.progressTimer) { - sendTelemetryEvent( - PYTHON_LANGUAGE_SERVER_ANALYSISTIME, - this.progressTimer.elapsedTime, - { success } - ); + + public dispose() { + if (this.statusBarMessage) { + this.statusBarMessage.dispose(); + } } - this.progressTimer = undefined; - this.progressTimeout = undefined; - } - // tslint:disable-next-line:no-any - private handleTimeout(_args: any[]): void { - this.completeAnalysisTracking(false); - } + private beginProgress(): void { + this.progressDeferred = createDeferred<void>(); + + window.withProgress( + { + location: ProgressLocation.Window, + title: '', + }, + (progress) => { + this.progress = progress; + return this.progressDeferred!.promise; + }, + ); + } } 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<void> { + 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 6108c85cd43d..875afa12f0b4 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -1,38 +1,43 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - -import { INugetRepository } from '../common/nuget/types'; -import { BANNER_NAME_DS_SURVEY, BANNER_NAME_LS_SURVEY, BANNER_NAME_PROPOSE_LS, IPythonExtensionBanner } from '../common/types'; -import { DataScienceSurveyBanner } from '../datascience/dataScienceSurveyBanner'; import { IServiceManager } from '../ioc/types'; -import { LanguageServerSurveyBanner } from '../languageServices/languageServerSurveyBanner'; -import { ProposeLanguageServerBanner } from '../languageServices/proposeLanguageServerBanner'; -import { ExtensionActivationService } from './activationService'; -import { DownloadBetaChannelRule, DownloadDailyChannelRule, DownloadStableChannelRule } from './downloadChannelRules'; -import { JediExtensionActivator } from './jedi'; -import { LanguageServerExtensionActivator } from './languageServer/languageServer'; -import { LanguageServerCompatibilityService } from './languageServer/languageServerCompatibilityService'; -import { LanguageServerFolderService } from './languageServer/languageServerFolderService'; -import { BetaLanguageServerPackageRepository, DailyLanguageServerPackageRepository, LanguageServerDownloadChannel, StableLanguageServerPackageRepository } from './languageServer/languageServerPackageRepository'; -import { LanguageServerPackageService } from './languageServer/languageServerPackageService'; -import { ExtensionActivators, IDownloadChannelRule, IExtensionActivationService, IExtensionActivator, ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, ILanguageServerFolderService, ILanguageServerPackageService } from './types'; +import { ExtensionActivationManager } from './activationManager'; +import { ExtensionSurveyPrompt } from './extensionSurvey'; +import { LanguageServerOutputChannel } from './common/outputChannel'; +import { + IExtensionActivationManager, + IExtensionActivationService, + IExtensionSingleActivationService, + ILanguageServerOutputChannel, +} 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'; + +export function registerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingleton<IExtensionActivationService>(IExtensionActivationService, PartialModeStatusItem); + serviceManager.add<IExtensionActivationManager>(IExtensionActivationManager, ExtensionActivationManager); + serviceManager.addSingleton<ILanguageServerOutputChannel>( + ILanguageServerOutputChannel, + LanguageServerOutputChannel, + ); + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + ExtensionSurveyPrompt, + ); + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + LoadLanguageServerExtension, + ); + + serviceManager.addSingleton<ILanguageServerWatcher>(ILanguageServerWatcher, LanguageServerWatcher); + serviceManager.addBinding(ILanguageServerWatcher, IExtensionActivationService); -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton<IExtensionActivationService>(IExtensionActivationService, ExtensionActivationService); - serviceManager.add<IExtensionActivator>(IExtensionActivator, JediExtensionActivator, ExtensionActivators.Jedi); - serviceManager.add<IExtensionActivator>(IExtensionActivator, LanguageServerExtensionActivator, ExtensionActivators.DotNet); - serviceManager.addSingleton<IPythonExtensionBanner>(IPythonExtensionBanner, LanguageServerSurveyBanner, BANNER_NAME_LS_SURVEY); - serviceManager.addSingleton<IPythonExtensionBanner>(IPythonExtensionBanner, ProposeLanguageServerBanner, BANNER_NAME_PROPOSE_LS); - serviceManager.addSingleton<IPythonExtensionBanner>(IPythonExtensionBanner, DataScienceSurveyBanner, BANNER_NAME_DS_SURVEY); - serviceManager.addSingleton<ILanguageServerFolderService>(ILanguageServerFolderService, LanguageServerFolderService); - serviceManager.addSingleton<ILanguageServerPackageService>(ILanguageServerPackageService, LanguageServerPackageService); - serviceManager.addSingleton<INugetRepository>(INugetRepository, StableLanguageServerPackageRepository, LanguageServerDownloadChannel.stable); - serviceManager.addSingleton<INugetRepository>(INugetRepository, BetaLanguageServerPackageRepository, LanguageServerDownloadChannel.beta); - serviceManager.addSingleton<INugetRepository>(INugetRepository, DailyLanguageServerPackageRepository, LanguageServerDownloadChannel.daily); - serviceManager.addSingleton<IDownloadChannelRule>(IDownloadChannelRule, DownloadDailyChannelRule, LanguageServerDownloadChannel.daily); - serviceManager.addSingleton<IDownloadChannelRule>(IDownloadChannelRule, DownloadBetaChannelRule, LanguageServerDownloadChannel.beta); - serviceManager.addSingleton<IDownloadChannelRule>(IDownloadChannelRule, DownloadStableChannelRule, LanguageServerDownloadChannel.stable); - serviceManager.addSingleton<ILanagueServerCompatibilityService>(ILanagueServerCompatibilityService, LanguageServerCompatibilityService); + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + RequirementsTxtLinkActivator, + ); } diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 98ff23dad689..e3b9b818691a 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -1,64 +1,110 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Request as RequestResult } from 'request'; -import { SemVer } from 'semver'; -import { NugetPackage } from '../common/nuget/types'; -import { IExtensionContext, LanguageServerDownloadChannels } from '../common/types'; - -export const IExtensionActivationService = Symbol('IExtensionActivationService'); -export interface IExtensionActivationService { - activate(): Promise<void>; -} - -export enum ExtensionActivators { - Jedi = 'Jedi', - DotNet = 'DotNet' -} - -export const IExtensionActivator = Symbol('IExtensionActivator'); -export interface IExtensionActivator { - activate(): Promise<boolean>; - deactivate(): Promise<void>; -} - -export const IHttpClient = Symbol('IHttpClient'); -export interface IHttpClient { - downloadFile(uri: string): Promise<RequestResult>; - getJSON<T>(uri: string): Promise<T>; -} - -export type FolderVersionPair = { path: string; version: SemVer }; -export const ILanguageServerFolderService = Symbol('ILanguageServerFolderService'); - -export interface ILanguageServerFolderService { - getLanguageServerFolderName(): Promise<string>; - getLatestLanguageServerVersion(): Promise<NugetPackage | undefined>; - getCurrentLanguageServerDirectory(): Promise<FolderVersionPair | undefined>; -} - -export const ILanguageServerDownloader = Symbol('ILanguageServerDownloader'); - -export interface ILanguageServerDownloader { - getDownloadInfo(): Promise<NugetPackage>; - downloadLanguageServer(context: IExtensionContext): Promise<void>; -} - -export const ILanguageServerPackageService = Symbol('ILanguageServerPackageService'); -export interface ILanguageServerPackageService { - getNugetPackageName(): string; - getLatestNugetPackageVersion(): Promise<NugetPackage>; - getLanguageServerDownloadChannel(): LanguageServerDownloadChannels; -} - -export const MajorLanguageServerVersion = Symbol('MajorLanguageServerVersion'); -export const IDownloadChannelRule = Symbol('IDownloadChannelRule'); -export interface IDownloadChannelRule { - shouldLookForNewLanguageServer(currentFolder?: FolderVersionPair): Promise<boolean>; -} -export const ILanguageServerCompatibilityService = Symbol('ILanguageServerCompatibilityService'); -export interface ILanguageServerCompatibilityService { - isSupported(): Promise<boolean>; -} +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Event } from 'vscode'; +import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node'; +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 { + // Method invoked when extension activates (invoked once). + activate(startupStopWatch: StopWatch): Promise<void>; + /** + * 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). + */ + activateWorkspace(resource: Resource): Promise<void>; +} + +export const IExtensionActivationService = Symbol('IExtensionActivationService'); +/** + * Classes implementing this interface will have their `activate` methods + * 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 { + supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean }; + activate(resource: Resource, startupStopWatch?: StopWatch): Promise<void>; +} + +export enum LanguageServerType { + Jedi = 'Jedi', + JediLSP = 'JediLSP', + Microsoft = 'Microsoft', + Node = 'Pylance', + None = 'None', +} + +export const ILanguageServerActivator = Symbol('ILanguageServerActivator'); +export interface ILanguageServerActivator { + start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise<void>; + activate(): void; + deactivate(): void; +} + +export const ILanguageClientFactory = Symbol('ILanguageClientFactory'); +export interface ILanguageClientFactory { + createLanguageClient( + resource: Resource, + interpreter: PythonEnvironment | undefined, + clientOptions: LanguageClientOptions, + env?: NodeJS.ProcessEnv, + ): Promise<LanguageClient>; +} +export const ILanguageServerAnalysisOptions = Symbol('ILanguageServerAnalysisOptions'); +export interface ILanguageServerAnalysisOptions extends IDisposable { + readonly onDidChange: Event<void>; + initialize(resource: Resource, interpreter: PythonEnvironment | undefined): Promise<void>; + getAnalysisOptions(): Promise<LanguageClientOptions>; +} +export const ILanguageServerManager = Symbol('ILanguageServerManager'); +export interface ILanguageServerManager extends IDisposable { + start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise<void>; + connect(): void; + disconnect(): void; +} + +export const ILanguageServerProxy = Symbol('ILanguageServerProxy'); +export interface ILanguageServerProxy extends IDisposable { + start( + resource: Resource, + interpreter: PythonEnvironment | undefined, + options: LanguageClientOptions, + ): Promise<void>; + stop(): Promise<void>; + /** + * 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. + */ + loadExtension(args?: unknown): void; +} + +export const ILanguageServerOutputChannel = Symbol('ILanguageServerOutputChannel'); +export interface ILanguageServerOutputChannel { + /** + * Creates output channel if necessary and returns it + */ + readonly channel: ILogOutputChannel; +} + +export const IExtensionSingleActivationService = Symbol('IExtensionSingleActivationService'); +/** + * Classes implementing this interface will have their `activate` methods + * 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 { + supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean }; + activate(): Promise<void>; +} diff --git a/src/client/api.ts b/src/client/api.ts index 71c857e6e903..908da4be7103 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -1,49 +1,169 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; -import { traceError } from './common/logger'; -import { RemoteDebuggerLauncherScriptProvider } from './debugger/debugAdapter/DebugClients/launcherProvider'; +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 { 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<void>, + serviceManager: IServiceManager, + serviceContainer: IServiceContainer, + discoveryApi: IDiscoveryAPI, +): PythonExtension { + const configurationService = serviceContainer.get<IConfigurationService>(IConfigurationService); + const interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService); + serviceManager.addSingleton<JupyterExtensionIntegration>(JupyterExtensionIntegration, JupyterExtensionIntegration); + serviceManager.addSingleton<JupyterExtensionPythonEnvironments>( + JupyterExtensionPythonEnvironments, + JupyterExtensionPythonEnvironments, + ); + serviceManager.addSingleton<TensorboardExtensionIntegration>( + TensorboardExtensionIntegration, + TensorboardExtensionIntegration, + ); + const jupyterPythonEnvApi = serviceContainer.get<JupyterPythonEnvironmentApi>(JupyterExtensionPythonEnvironments); + const environments = buildEnvironmentApi(discoveryApi, serviceContainer, jupyterPythonEnvApi); + const jupyterIntegration = serviceContainer.get<JupyterExtensionIntegration>(JupyterExtensionIntegration); + jupyterIntegration.registerEnvApi(environments); + const tensorboardIntegration = serviceContainer.get<TensorboardExtensionIntegration>( + TensorboardExtensionIntegration, + ); + const outputChannel = serviceContainer.get<ILanguageServerOutputChannel>(ILanguageServerOutputChannel); -export interface IExtensionApi { - /** - * Promise indicating whether all parts of the extension have completed loading or not. - * @type {Promise<void>} - * @memberof IExtensionApi - */ - ready: Promise<void>; - 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/ptvsd_launcher.py', '--host', 'localhost', '--port', '57039', '--wait']` - * @param {string} host - * @param {number} port - * @param {boolean} [waitUntilDebuggerAttaches=true] - * @returns {Promise<string[]>} + * Internal API just for Jupyter, hence don't include in the official types. */ - getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise<string[]>; - }; -} - -// tslint:disable-next-line:no-any -export function buildApi(ready: Promise<any>) { - return { - // 'ready' will propogate the exception, but we must log it here first. + jupyter: { + registerHooks(): void; + }; + /** + * Internal API just for Tensorboard, hence don't include in the official types. + */ + tensorboard: { + registerHooks(): void; + }; + } & { + /** + * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an + * iteration or two. + */ + pylance: ApiForPylance; + } & { + /** + * @deprecated Use PythonExtension.environments API instead. + * + * Return internal settings within the extension which are stored in VSCode storage + */ + settings: { + /** + * An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes. + */ + readonly onDidChangeExecutionDetails: Event<Uri | undefined>; + /** + * 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, + ): { + /** + * E.g of execution commands returned could be, + * * `['<path to the interpreter set in settings>']` + * * `['<path to the interpreter selected by the extension when setting is not set>']` + * * `['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; + }; + }; + } = { + // '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): Promise<string[]> { - return new RemoteDebuggerLauncherScriptProvider().getLauncherArgs({ host, port, waitUntilDebuggerAttaches }); - } - } + async getRemoteLauncherCommand( + host: string, + port: number, + waitUntilDebuggerAttaches = true, + ): Promise<string[]> { + return getDebugpyLauncherArgs({ + host, + port, + waitUntilDebuggerAttaches, + }); + }, + async getDebuggerPackagePath(): Promise<string | undefined> { + return getDebugpyPath(); + }, + }, + settings: { + onDidChangeExecutionDetails: interpreterService.onDidChangeInterpreterConfiguration, + getExecutionDetails(resource?: Resource) { + const { pythonPath } = configurationService.getSettings(resource); + // If pythonPath equals an empty string, no interpreter is set. + return { execCommand: pythonPath === '' ? undefined : [pythonPath] }; + }, + }, + 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); + }, + start: (client: BaseLanguageClient): Promise<void> => client.start(), + stop: (client: BaseLanguageClient): Promise<void> => client.stop(), + getTelemetryReporter: () => getTelemetryReporter(), + }, + environments, }; + + // In test environment return the DI Container. + if (isTestExecution()) { + (api as any).serviceContainer = serviceContainer; + (api as any).serviceManager = serviceManager; + } + 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<void>; + 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<string[]>; + + /** + * Gets the path to the debugger package used by the extension. + */ + getDebuggerPackagePath(): Promise<string | undefined>; + }; + + /** + * 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<void>; + /** + * This event is triggered when the active environment setting changes. + */ + readonly onDidChangeActiveEnvironmentPath: Event<ActiveEnvironmentPathChangeEvent>; + /** + * 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<EnvironmentsChangeEvent>; + /** + * 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<void>; + /** + * 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<ResolvedEnvironment | undefined>; + /** + * 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<EnvironmentVariablesChangeEvent>; + }; +} + +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<PythonExtension> { + 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 e270305f424e..90d2ced8d0ae 100644 --- a/src/client/application/diagnostics/applicationDiagnostics.ts +++ b/src/client/application/diagnostics/applicationDiagnostics.ts @@ -1,52 +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 { STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; -import { ILogger, IOutputChannel } 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'; -@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>(ISourceMapSupportService).register(); - } - public async performPreStartupHealthCheck(): Promise<void> { - const diagnosticsServices = this.serviceContainer.getAll<IDiagnosticsService>(IDiagnosticsService); - await Promise.all(diagnosticsServices.map(async diagnosticsService => { - const diagnostics = await diagnosticsService.diagnose(); - this.log(diagnostics); - if (diagnostics.length > 0) { - await diagnosticsService.handle(diagnostics); +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; } - })); - } - private log(diagnostics: IDiagnostic[]): void { - const logger = this.serviceContainer.get<ILogger>(ILogger); - diagnostics.forEach(item => { - const message = `Diagnostic Code: ${item.code}, Message: ${item.message}`; - switch (item.severity) { - case DiagnosticSeverity.Error: { - logger.logError(message); - this.outputChannel.appendLine(message); - break; - } - case DiagnosticSeverity.Warning: { - logger.logWarning(message); - this.outputChannel.appendLine(message); - break; - } - default: { - logger.logInformation(message); - } + default: { + traceVerbose(message); } - }); + } + }); +} + +async function runDiagnostics(diagnosticServices: IDiagnosticsService[], resource: Resource): Promise<void> { + 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) {} + + public register() {} + + public async performPreStartupHealthCheck(resource: Resource): Promise<void> { + // When testing, do not perform health checks, as modal dialogs can be displayed. + if (isTestExecution()) { + return; + } + let services = this.serviceContainer.getAll<IDiagnosticsService>(IDiagnosticsService); + const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); + if (!workspaceService.isTrusted) { + services = services.filter((item) => item.runInUntrustedWorkspace); + } + // Perform these validation checks in the foreground. + await runDiagnostics( + services.filter((item) => !item.runInBackground), + resource, + ); + + // Perform these validation checks in the background. + runDiagnostics( + services.filter((item) => item.runInBackground), + resource, + ).ignoreErrors(); } } diff --git a/src/client/application/diagnostics/base.ts b/src/client/application/diagnostics/base.ts index 127620ed1f54..8ce1c3b83184 100644 --- a/src/client/application/diagnostics/base.ts +++ b/src/client/application/diagnostics/base.ts @@ -5,28 +5,83 @@ 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 { DIAGNOSTICS_MESSAGE } from '../../telemetry/constants'; +import { EventName } from '../../telemetry/constants'; +import { DiagnosticCodes } from './constants'; import { DiagnosticScope, IDiagnostic, IDiagnosticFilterService, IDiagnosticsService } from './types'; @injectable() export abstract class BaseDiagnostic implements IDiagnostic { - constructor(public readonly code: string, public readonly message: string, - public readonly severity: DiagnosticSeverity, public readonly scope: DiagnosticScope) { } + constructor( + public readonly code: DiagnosticCodes, + public readonly message: string, + public readonly severity: DiagnosticSeverity, + public readonly scope: DiagnosticScope, + public readonly resource: Resource, + public readonly shouldShowPrompt = true, + public readonly invokeHandler: 'always' | 'default' = 'default', + ) {} } @injectable() -export abstract class BaseDiagnosticsService implements IDiagnosticsService { +export abstract class BaseDiagnosticsService implements IDiagnosticsService, IDisposable { + protected static handledDiagnosticCodeKeys: string[] = []; protected readonly filterService: IDiagnosticFilterService; - constructor(@unmanaged() private readonly supportedDiagnosticCodes: string[], - @unmanaged() protected serviceContainer: IServiceContainer) { + constructor( + @unmanaged() private readonly supportedDiagnosticCodes: string[], + @unmanaged() protected serviceContainer: IServiceContainer, + @unmanaged() protected disposableRegistry: IDisposableRegistry, + @unmanaged() public readonly runInBackground: boolean = false, + @unmanaged() public readonly runInUntrustedWorkspace: boolean = false, + ) { this.filterService = serviceContainer.get<IDiagnosticFilterService>(IDiagnosticFilterService); + disposableRegistry.push(this); + } + public abstract diagnose(resource: Resource): Promise<IDiagnostic[]>; + public dispose() { + // Nothing to do, but can be overidden + } + public async handle(diagnostics: IDiagnostic[]): Promise<void> { + if (diagnostics.length === 0) { + return; + } + const diagnosticsToHandle = await asyncFilter(diagnostics, async (item) => { + if (!(await this.canHandle(item))) { + return false; + } + if (item.invokeHandler && item.invokeHandler === 'always') { + return true; + } + const key = this.getDiagnosticsKey(item); + if (BaseDiagnosticsService.handledDiagnosticCodeKeys.indexOf(key) !== -1) { + return false; + } + BaseDiagnosticsService.handledDiagnosticCodeKeys.push(key); + return true; + }); + await this.onHandle(diagnosticsToHandle); } - public abstract diagnose(): Promise<IDiagnostic[]>; - public abstract handle(diagnostics: IDiagnostic[]): Promise<void>; public async canHandle(diagnostic: IDiagnostic): Promise<boolean> { - sendTelemetryEvent(DIAGNOSTICS_MESSAGE, undefined, { code: diagnostic.code }); - return this.supportedDiagnosticCodes.filter(item => item === diagnostic.code).length > 0; + sendTelemetryEvent(EventName.DIAGNOSTICS_MESSAGE, undefined, { code: diagnostic.code }); + return this.supportedDiagnosticCodes.filter((item) => item === diagnostic.code).length > 0; + } + protected abstract onHandle(diagnostics: IDiagnostic[]): Promise<void>; + /** + * 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 getDiagnosticsKey(diagnostic: IDiagnostic): string { + if (diagnostic.scope === DiagnosticScope.Global) { + return diagnostic.code; + } + const workspace = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); + const workspaceFolder = diagnostic.resource ? workspace.getWorkspaceFolder(diagnostic.resource) : undefined; + return `${diagnostic.code}dbe75733-0407-4124-a1b2-ca769dc30523${ + workspaceFolder ? workspaceFolder.uri.fsPath : '' + }`; } } diff --git a/src/client/application/diagnostics/checks/envPathVariable.ts b/src/client/application/diagnostics/checks/envPathVariable.ts index bccb3a29c042..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, IPathUtils } from '../../../common/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'; @@ -16,13 +16,19 @@ import { DiagnosticCodes } from '../constants'; import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; -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.'; +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 { - constructor(message) { - super(DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic, - message, DiagnosticSeverity.Warning, DiagnosticScope.Global); +class InvalidEnvironmentPathVariableDiagnostic extends BaseDiagnostic { + constructor(message: string, resource: Resource) { + super( + DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic, + message, + DiagnosticSeverity.Warning, + DiagnosticScope.Global, + resource, + ); } } @@ -31,24 +37,37 @@ export const EnvironmentPathVariableDiagnosticsServiceId = 'EnvironmentPathVaria @injectable() export class EnvironmentPathVariableDiagnosticsService extends BaseDiagnosticsService { protected readonly messageService: IDiagnosticHandlerService<MessageCommandPrompt>; + private readonly platform: IPlatformService; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super([DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic], serviceContainer); + + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + ) { + super( + [DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic], + serviceContainer, + disposableRegistry, + true, + true, + ); this.platform = this.serviceContainer.get<IPlatformService>(IPlatformService); - this.messageService = serviceContainer.get<IDiagnosticHandlerService<MessageCommandPrompt>>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerServiceId); + this.messageService = serviceContainer.get<IDiagnosticHandlerService<MessageCommandPrompt>>( + IDiagnosticHandlerService, + DiagnosticCommandPromptHandlerServiceId, + ); } - public async diagnose(): Promise<IDiagnostic[]> { - if (this.platform.isWindows && - this.doesPathVariableHaveInvalidEntries()) { + + public async diagnose(resource: Resource): Promise<IDiagnostic[]> { + if (this.platform.isWindows && this.doesPathVariableHaveInvalidEntries()) { const env = this.serviceContainer.get<IApplicationEnvironment>(IApplicationEnvironment); - const message = InvalidEnvPathVariableMessage - .format(this.platform.pathVariableName, env.extensionName); - return [new InvalidEnvironmentPathVariableDiagnostic(message)]; - } else { - return []; + const message = InvalidEnvPathVariableMessage.format(this.platform.pathVariableName, env.extensionName); + return [new InvalidEnvironmentPathVariableDiagnostic(message, resource)]; } + return []; } - public async handle(diagnostics: IDiagnostic[]): Promise<void> { + + protected async onHandle(diagnostics: IDiagnostic[]): Promise<void> { // This class can only handle one type of diagnostic, hence just use first item in list. if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { return; @@ -60,25 +79,26 @@ export class EnvironmentPathVariableDiagnosticsService extends BaseDiagnosticsSe const commandFactory = this.serviceContainer.get<IDiagnosticsCommandFactory>(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>(ICurrentProcess); const pathValue = currentProc.env[this.platform.pathVariableName]; const pathSeparator = this.serviceContainer.get<IPathUtils>(IPathUtils).delimiter; - const paths = pathValue.split(pathSeparator); - return paths.filter(item => item.indexOf('"') >= 0).length > 0; + const paths = (pathValue || '').split(pathSeparator); + return paths.filter((item) => item.indexOf('"') >= 0).length > 0; } } diff --git a/src/client/application/diagnostics/checks/invalidDebuggerType.ts b/src/client/application/diagnostics/checks/invalidDebuggerType.ts deleted file mode 100644 index a57677f26590..000000000000 --- a/src/client/application/diagnostics/checks/invalidDebuggerType.ts +++ /dev/null @@ -1,115 +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 { DiagnosticSeverity, WorkspaceFolder } from 'vscode'; -import { ICommandManager, IWorkspaceService } from '../../../common/application/types'; -import '../../../common/extensions'; -import { IFileSystem } from '../../../common/platform/types'; -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'; - -const InvalidDebuggerTypeMessage = '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 class InvalidDebuggerTypeDiagnostic extends BaseDiagnostic { - constructor(message) { - super(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - message, DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder); - } -} - -export const InvalidDebuggerTypeDiagnosticsServiceId = 'InvalidDebuggerTypeDiagnosticsServiceId'; - -const CommandName = 'python.debugger.replaceExperimental'; - -@injectable() -export class InvalidDebuggerTypeDiagnosticsService extends BaseDiagnosticsService { - protected readonly messageService: IDiagnosticHandlerService<MessageCommandPrompt>; - protected readonly fs: IFileSystem; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super([DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic], serviceContainer); - this.messageService = serviceContainer.get<IDiagnosticHandlerService<MessageCommandPrompt>>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerServiceId); - const cmdManager = serviceContainer.get<ICommandManager>(ICommandManager); - this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem); - cmdManager.registerCommand(CommandName, this.fixLaunchJson, this); - } - public async diagnose(): Promise<IDiagnostic[]> { - if (await this.isExperimentalDebuggerUsed()) { - return [new InvalidDebuggerTypeDiagnostic(InvalidDebuggerTypeMessage)]; - } else { - return []; - } - } - public async handle(diagnostics: IDiagnostic[]): Promise<void> { - // This class can only handle one type of diagnostic, hence just use first item in list. - if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { - return; - } - const diagnostic = diagnostics[0]; - const commandFactory = this.serviceContainer.get<IDiagnosticsCommandFactory>(IDiagnosticsCommandFactory); - const options = [ - { - prompt: 'Yes, update launch.json', - command: commandFactory.createCommand(diagnostic, { type: 'executeVSCCommand', options: 'python.debugger.replaceExperimental' }) - }, - { - prompt: 'No, I will do it later' - } - ]; - - await this.messageService.handle(diagnostic, { commandPrompts: options }); - } - private async isExperimentalDebuggerUsed() { - const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - if (!workspaceService.hasWorkspaceFolders) { - return false; - } - - const results = await Promise.all(workspaceService.workspaceFolders!.map(workspaceFolder => this.isExperimentalDebuggerUsedInWorkspace(workspaceFolder))); - return results.filter(used => used === true).length > 0; - } - private getLaunchJsonFile(workspaceFolder: WorkspaceFolder) { - return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); - } - private async isExperimentalDebuggerUsedInWorkspace(workspaceFolder: WorkspaceFolder) { - const launchJson = this.getLaunchJsonFile(workspaceFolder); - if (!await this.fs.fileExists(launchJson)) { - return false; - } - - const fileContents = await this.fs.readFile(launchJson); - return fileContents.indexOf('"pythonExperimental"') > 0; - } - private async fixLaunchJson() { - const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - if (!workspaceService.hasWorkspaceFolders) { - return false; - } - - await Promise.all(workspaceService.workspaceFolders!.map(workspaceFolder => this.fixLaunchJsonInWorkspace(workspaceFolder))); - } - private async fixLaunchJsonInWorkspace(workspaceFolder: WorkspaceFolder) { - if (!await this.isExperimentalDebuggerUsedInWorkspace(workspaceFolder)) { - return; - } - - const launchJson = this.getLaunchJsonFile(workspaceFolder); - let fileContents = await this.fs.readFile(launchJson); - const debuggerType = new RegExp('"pythonExperimental"', 'g'); - const debuggerLabel = new RegExp('"Python Experimental:', 'g'); - - fileContents = fileContents.replace(debuggerType, '"python"'); - fileContents = fileContents.replace(debuggerLabel, '"Python:'); - - await this.fs.writeFile(launchJson, fileContents); - } -} diff --git a/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts new file mode 100644 index 000000000000..440ff16856d3 --- /dev/null +++ b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// eslint-disable-next-line max-classes-per-file +import { inject, injectable, named } from 'inversify'; +import * as path from 'path'; +import { DiagnosticSeverity, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import '../../../common/extensions'; +import { IFileSystem } from '../../../common/platform/types'; +import { IDisposableRegistry, Resource } from '../../../common/types'; +import { Common, Diagnostics } 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'; + +const messages = { + [DiagnosticCodes.InvalidDebuggerTypeDiagnostic]: Diagnostics.invalidDebuggerTypeDiagnostic, + [DiagnosticCodes.JustMyCodeDiagnostic]: Diagnostics.justMyCodeDiagnostic, + [DiagnosticCodes.ConsoleTypeDiagnostic]: Diagnostics.consoleTypeDiagnostic, + [DiagnosticCodes.ConfigPythonPathDiagnostic]: '', +}; + +export class InvalidLaunchJsonDebuggerDiagnostic extends BaseDiagnostic { + constructor( + code: + | DiagnosticCodes.InvalidDebuggerTypeDiagnostic + | DiagnosticCodes.JustMyCodeDiagnostic + | DiagnosticCodes.ConsoleTypeDiagnostic + | DiagnosticCodes.ConfigPythonPathDiagnostic, + resource: Resource, + shouldShowPrompt = true, + ) { + super( + code, + messages[code], + DiagnosticSeverity.Error, + DiagnosticScope.WorkspaceFolder, + resource, + shouldShowPrompt, + ); + } +} + +export const InvalidLaunchJsonDebuggerServiceId = 'InvalidLaunchJsonDebuggerServiceId'; + +@injectable() +export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IDiagnosticHandlerService) + @named(DiagnosticCommandPromptHandlerServiceId) + private readonly messageService: IDiagnosticHandlerService<MessageCommandPrompt>, + ) { + super( + [ + DiagnosticCodes.InvalidDebuggerTypeDiagnostic, + DiagnosticCodes.JustMyCodeDiagnostic, + DiagnosticCodes.ConsoleTypeDiagnostic, + DiagnosticCodes.ConfigPythonPathDiagnostic, + ], + serviceContainer, + disposableRegistry, + true, + ); + } + + public async diagnose(resource: Resource): Promise<IDiagnostic[]> { + const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; + if (!hasWorkspaceFolders) { + return []; + } + const workspaceFolder = resource + ? this.workspaceService.getWorkspaceFolder(resource)! + : this.workspaceService.workspaceFolders![0]; + return this.diagnoseWorkspace(workspaceFolder, resource); + } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise<void> { + diagnostics.forEach((diagnostic) => this.handleDiagnostic(diagnostic)); + } + + protected async fixLaunchJson(code: DiagnosticCodes): Promise<void> { + const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; + if (!hasWorkspaceFolders) { + return; + } + + await Promise.all( + (this.workspaceService.workspaceFolders ?? []).map((workspaceFolder) => + this.fixLaunchJsonInWorkspace(code, workspaceFolder), + ), + ); + } + + private async diagnoseWorkspace(workspaceFolder: WorkspaceFolder, resource: Resource) { + const launchJson = getLaunchJsonFile(workspaceFolder); + if (!(await this.fs.fileExists(launchJson))) { + return []; + } + + const fileContents = await this.fs.readFile(launchJson); + const diagnostics: IDiagnostic[] = []; + if (fileContents.indexOf('"pythonExperimental"') > 0) { + diagnostics.push( + new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, resource), + ); + } + if (fileContents.indexOf('"debugStdLib"') > 0) { + diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, resource)); + } + if (fileContents.indexOf('"console": "none"') > 0) { + 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), + ); + } + return diagnostics; + } + + private async handleDiagnostic(diagnostic: IDiagnostic): Promise<void> { + if (!diagnostic.shouldShowPrompt) { + await this.fixLaunchJson(diagnostic.code); + return; + } + const commandPrompts = [ + { + prompt: Diagnostics.yesUpdateLaunch, + command: { + diagnostic, + invoke: async (): Promise<void> => { + await this.fixLaunchJson(diagnostic.code); + }, + }, + }, + { + 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 = getLaunchJsonFile(workspaceFolder); + let fileContents = await this.fs.readFile(launchJson); + switch (code) { + case DiagnosticCodes.InvalidDebuggerTypeDiagnostic: { + fileContents = findAndReplace(fileContents, '"pythonExperimental"', '"python"'); + fileContents = findAndReplace(fileContents, '"Python Experimental:', '"Python:'); + break; + } + case DiagnosticCodes.JustMyCodeDiagnostic: { + fileContents = findAndReplace(fileContents, '"debugStdLib": false', '"justMyCode": true'); + fileContents = findAndReplace(fileContents, '"debugStdLib": true', '"justMyCode": false'); + break; + } + case DiagnosticCodes.ConsoleTypeDiagnostic: { + fileContents = findAndReplace(fileContents, '"console": "none"', '"console": "internalConsole"'); + break; + } + case DiagnosticCodes.ConfigPythonPathDiagnostic: { + fileContents = findAndReplace(fileContents, '"pythonPath":', '"python":'); + fileContents = findAndReplace( + fileContents, + '{config:python.pythonPath}', + '{command:python.interpreterPath}', + ); + fileContents = findAndReplace( + fileContents, + '{config:python.interpreterPath}', + '{command:python.interpreterPath}', + ); + break; + } + default: { + return; + } + } + + await this.fs.writeFile(launchJson, fileContents); + } +} + +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 fc391a704361..f08c09956838 100644 --- a/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts +++ b/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts @@ -1,83 +1,175 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - -import { inject, injectable } from 'inversify'; -import { DiagnosticSeverity, Uri } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; +// 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 { Logger, traceError } from '../../../common/logger'; -import { IConfigurationService } from '../../../common/types'; +import { IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; +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'; import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; -import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService, IInvalidPythonPathInDebuggerService } from '../types'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticHandlerService, + IInvalidPythonPathInDebuggerService, +} from '../types'; -const InvalidPythonPathInDebuggerMessage = 'You need to select a Python interpreter before you start debugging.\n\nTip: click on "Select Python Interpreter" in the status bar.'; +const messages = { + [DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic]: Diagnostics.invalidPythonPathInDebuggerSettings, + [DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic]: Diagnostics.invalidPythonPathInDebuggerLaunch, +}; -export class InvalidPythonPathInDebuggerDiagnostic extends BaseDiagnostic { - constructor() { - super(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - InvalidPythonPathInDebuggerMessage, DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder); +class InvalidPythonPathInDebuggerDiagnostic extends BaseDiagnostic { + constructor( + code: + | DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic + | DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, + resource: Resource, + ) { + super( + code, + messages[code], + DiagnosticSeverity.Error, + DiagnosticScope.WorkspaceFolder, + resource, + undefined, + 'always', + ); } } export const InvalidPythonPathInDebuggerServiceId = 'InvalidPythonPathInDebuggerServiceId'; -const CommandName = 'python.setInterpreter'; - @injectable() -export class InvalidPythonPathInDebuggerService extends BaseDiagnosticsService implements IInvalidPythonPathInDebuggerService { - protected readonly messageService: IDiagnosticHandlerService<MessageCommandPrompt>; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer, +export class InvalidPythonPathInDebuggerService extends BaseDiagnosticsService + implements IInvalidPythonPathInDebuggerService { + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, @inject(IDiagnosticsCommandFactory) private readonly commandFactory: IDiagnosticsCommandFactory, @inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper, - @inject(IConfigurationService) private readonly configService: IConfigurationService) { - super([DiagnosticCodes.InvalidPythonPathInDebuggerDiagnostic], serviceContainer); - this.messageService = serviceContainer.get<IDiagnosticHandlerService<MessageCommandPrompt>>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerServiceId); + @inject(IDocumentManager) private readonly documentManager: IDocumentManager, + @inject(IConfigurationService) private readonly configService: IConfigurationService, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IDiagnosticHandlerService) + @named(DiagnosticCommandPromptHandlerServiceId) + protected readonly messageService: IDiagnosticHandlerService<MessageCommandPrompt>, + ) { + super( + [ + DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, + DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic, + ], + serviceContainer, + disposableRegistry, + true, + ); } + + // eslint-disable-next-line class-methods-use-this public async diagnose(): Promise<IDiagnostic[]> { return []; } - public async handle(diagnostics: IDiagnostic[]): Promise<void> { - // This class can only handle one type of diagnostic, hence just use first item in list. - if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { - return; - } - const diagnostic = diagnostics[0]; - const options = [ - { - prompt: 'Select Python Interpreter', - command: this.commandFactory.createCommand(diagnostic, { type: 'executeVSCCommand', options: CommandName }) - } - ]; - await this.messageService.handle(diagnostic, { commandPrompts: options }); - } - public async validatePythonPath(pythonPath?: string, resource?: Uri) { + public async validatePythonPath( + pythonPath?: string, + pythonPathSource?: PythonPathSource, + resource?: Uri, + ): Promise<boolean> { pythonPath = pythonPath ? this.resolveVariables(pythonPath, resource) : undefined; - // tslint:disable-next-line:no-invalid-template-strings - if (pythonPath === '${config:python.pythonPath}' || !pythonPath) { + + if (pythonPath === '${command:python.interpreterPath}' || !pythonPath) { pythonPath = this.configService.getSettings(resource).pythonPath; } if (await this.interpreterHelper.getInterpreterInformation(pythonPath).catch(() => undefined)) { return true; } traceError(`Invalid Python Path '${pythonPath}'`); - this.handle([new InvalidPythonPathInDebuggerDiagnostic()]) - .catch(ex => Logger.error('Failed to handle invalid python path in debugger', ex)) - .ignoreErrors(); + if (pythonPathSource === PythonPathSource.launchJson) { + this.handle([ + new InvalidPythonPathInDebuggerDiagnostic( + DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic, + resource, + ), + ]) + .catch((ex) => traceError('Failed to handle invalid python path in launch.json debugger', ex)) + .ignoreErrors(); + } else { + this.handle([ + new InvalidPythonPathInDebuggerDiagnostic( + DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, + resource, + ), + ]) + .catch((ex) => traceError('Failed to handle invalid python path in settings.json debugger', ex)) + .ignoreErrors(); + } return false; } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise<void> { + // This class can only handle one type of diagnostic, hence just use first item in list. + if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { + return; + } + const diagnostic = diagnostics[0]; + const commandPrompts = this.getCommandPrompts(diagnostic); + + await this.messageService.handle(diagnostic, { commandPrompts }); + } + protected resolveVariables(pythonPath: string, resource: Uri | undefined): string { - const workspaceFolder = resource ? this.workspace.getWorkspaceFolder(resource) : undefined; - const systemVariables = new SystemVariables(workspaceFolder ? workspaceFolder.uri.fsPath : undefined); + 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: Common.selectPythonInterpreter, + command: this.commandFactory.createCommand(diagnostic, { + type: 'executeVSCCommand', + options: 'python.setInterpreter', + }), + }, + ]; + } + case DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic: { + return [ + { + prompt: Common.openLaunch, + command: { + diagnostic, + invoke: async (): Promise<void> => { + const launchJson = getLaunchJsonFile(workspc.workspaceFolders![0]); + const doc = await this.documentManager.openTextDocument(launchJson); + await this.documentManager.showTextDocument(doc); + }, + }, + }, + ]; + } + default: { + throw new Error("Invalid diagnostic for 'InvalidPythonPathInDebuggerService'"); + } + } + } +} + +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<MessageCommandPrompt>, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + ) { + super([DiagnosticCodes.JediPython27NotSupportedDiagnostic], serviceContainer, disposableRegistry, true); + } + + public async diagnose(resource: Resource): Promise<IDiagnostic[]> { + 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<void> { + 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>(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<void | undefined> { + // Update settings.json value to Jedi if it's JediLSP. + const settings = this.workspaceService + .getConfiguration('python', resource) + .inspect<LanguageServerType>('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 9aeaec5884d5..000000000000 --- a/src/client/application/diagnostics/checks/lsNotSupported.ts +++ /dev/null @@ -1,61 +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 { 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) { - super(DiagnosticCodes.LSNotSupportedDiagnostic, - message, DiagnosticSeverity.Warning, DiagnosticScope.Global); - } -} - -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<MessageCommandPrompt>) { - super([DiagnosticCodes.LSNotSupportedDiagnostic], serviceContainer); - } - public async diagnose(): Promise<IDiagnostic[]>{ - if (await this.lsCompatibility.isSupported()) { - return []; - } else{ - return [new LSNotSupportedDiagnostic(Diagnostics.lsNotSupported())]; - } - } - public async handle(diagnostics: IDiagnostic[]): Promise<void>{ - 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>(IDiagnosticsCommandFactory); - const options = [ - { - prompt: 'More Info', - command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://aka.ms/AA3qqka' }) - }, - { - 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 new file mode 100644 index 000000000000..21d6b34fb7c5 --- /dev/null +++ b/src/client/application/diagnostics/checks/macPythonInterpreter.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// eslint-disable-next-line max-classes-per-file +import { inject, injectable } from 'inversify'; +import { DiagnosticSeverity, l10n } from 'vscode'; +import '../../../common/extensions'; +import { IPlatformService } from '../../../common/platform/types'; +import { + IConfigurationService, + IDisposableRegistry, + IInterpreterPathService, + InterpreterConfigurationScope, + Resource, +} from '../../../common/types'; +import { IInterpreterHelper } 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, IDiagnosticCommand, IDiagnosticHandlerService } from '../types'; +import { Common } from '../../../common/utils/localize'; + +const messages = { + [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.MacInterpreterSelected, resource: Resource) { + super(code, messages[code], DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder, resource); + } +} + +export const InvalidMacPythonInterpreterServiceId = 'InvalidMacPythonInterpreterServiceId'; + +@injectable() +export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { + protected changeThrottleTimeout = 1000; + + private timeOut?: NodeJS.Timeout | number; + + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, + ) { + super([DiagnosticCodes.MacInterpreterSelected], serviceContainer, disposableRegistry, true); + this.addPythonPathChangedHandler(); + } + + public dispose(): void { + if (this.timeOut && typeof this.timeOut !== 'number') { + clearTimeout(this.timeOut); + this.timeOut = undefined; + } + } + + public async diagnose(resource: Resource): Promise<IDiagnostic[]> { + if (!this.platform.isMac) { + return []; + } + const configurationService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); + const settings = configurationService.getSettings(resource); + if (!(await this.helper.isMacDefaultPythonPath(settings.pythonPath))) { + return []; + } + return [new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelected, resource)]; + } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise<void> { + if (diagnostics.length === 0) { + return; + } + const messageService = this.serviceContainer.get<IDiagnosticHandlerService<MessageCommandPrompt>>( + IDiagnosticHandlerService, + DiagnosticCommandPromptHandlerServiceId, + ); + await Promise.all( + diagnostics.map(async (diagnostic) => { + const canHandle = await this.canHandle(diagnostic); + const shouldIgnore = await this.filterService.shouldIgnoreDiagnostic(diagnostic.code); + if (!canHandle || shouldIgnore) { + return; + } + const commandPrompts = this.getCommandPrompts(diagnostic); + await messageService.handle(diagnostic, { commandPrompts, message: diagnostic.message }); + }), + ); + } + + protected addPythonPathChangedHandler(): void { + const disposables = this.serviceContainer.get<IDisposableRegistry>(IDisposableRegistry); + const interpreterPathService = this.serviceContainer.get<IInterpreterPathService>(IInterpreterPathService); + disposables.push(interpreterPathService.onDidChange((i) => this.onDidChangeConfiguration(i))); + } + + protected async onDidChangeConfiguration( + interpreterConfigurationScope: InterpreterConfigurationScope, + ): Promise<void> { + const workspaceUri = interpreterConfigurationScope.uri; + // Lets wait, for more changes, dirty simple throttling. + if (this.timeOut && typeof this.timeOut !== 'number') { + clearTimeout(this.timeOut); + this.timeOut = undefined; + } + this.timeOut = setTimeout(() => { + this.timeOut = undefined; + this.diagnose(workspaceUri) + .then((diagnostics) => this.handle(diagnostics)) + .ignoreErrors(); + }, this.changeThrottleTimeout); + } + + private getCommandPrompts(diagnostic: IDiagnostic): { prompt: string; command?: IDiagnosticCommand }[] { + const commandFactory = this.serviceContainer.get<IDiagnosticsCommandFactory>(IDiagnosticsCommandFactory); + switch (diagnostic.code) { + case DiagnosticCodes.MacInterpreterSelected: { + return [ + { + prompt: Common.selectPythonInterpreter, + command: commandFactory.createCommand(diagnostic, { + type: 'executeVSCCommand', + options: 'python.setInterpreter', + }), + }, + { + prompt: Common.doNotShowAgain, + command: commandFactory.createCommand(diagnostic, { + type: 'ignore', + options: DiagnosticScope.Global, + }), + }, + ]; + } + default: { + throw new Error("Invalid diagnostic for 'InvalidMacPythonInterpreterService'"); + } + } + } +} diff --git a/src/client/application/diagnostics/checks/powerShellActivation.ts b/src/client/application/diagnostics/checks/powerShellActivation.ts index 532ec869cde1..85f68db0d6a4 100644 --- a/src/client/application/diagnostics/checks/powerShellActivation.ts +++ b/src/client/application/diagnostics/checks/powerShellActivation.ts @@ -1,46 +1,70 @@ // 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 { Logger } from '../../../common/logger'; import { useCommandPromptAsDefaultShell } from '../../../common/terminal/commandPrompt'; -import { IConfigurationService, ICurrentProcess } from '../../../common/types'; +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 { DIAGNOSTICS_ACTION } from '../../../telemetry/constants'; +import { EventName } from '../../../telemetry/constants'; 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'; -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() { - super(DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic, + constructor(resource: Resource) { + super( + DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic, PowershellActivationNotSupportedWithBatchFilesMessage, - DiagnosticSeverity.Warning, DiagnosticScope.Global); + DiagnosticSeverity.Warning, + DiagnosticScope.Global, + resource, + undefined, + 'always', + ); } } -export const PowerShellActivationHackDiagnosticsServiceId = 'EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic'; +export const PowerShellActivationHackDiagnosticsServiceId = + 'EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic'; @injectable() export class PowerShellActivationHackDiagnosticsService extends BaseDiagnosticsService { protected readonly messageService: IDiagnosticHandlerService<MessageCommandPrompt>; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super([DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic], serviceContainer); - this.messageService = serviceContainer.get<IDiagnosticHandlerService<MessageCommandPrompt>>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerServiceId); + + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + ) { + super( + [DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic], + serviceContainer, + disposableRegistry, + true, + ); + this.messageService = serviceContainer.get<IDiagnosticHandlerService<MessageCommandPrompt>>( + IDiagnosticHandlerService, + DiagnosticCommandPromptHandlerServiceId, + ); } + + // eslint-disable-next-line class-methods-use-this public async diagnose(): Promise<IDiagnostic[]> { return []; } - public async handle(diagnostics: IDiagnostic[]): Promise<void> { + + protected async onHandle(diagnostics: IDiagnostic[]): Promise<void> { // This class can only handle one type of diagnostic, hence just use first item in list. if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { return; @@ -54,27 +78,34 @@ export class PowerShellActivationHackDiagnosticsService extends BaseDiagnosticsS const configurationService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); const options = [ { - prompt: 'Use Command Prompt', - // tslint:disable-next-line:no-object-literal-type-assertion + prompt: Common.useCommandPrompt, + command: { - diagnostic, invoke: async (): Promise<void> => { - sendTelemetryEvent(DIAGNOSTICS_ACTION, undefined, { action: 'switchToCommandPrompt' }); - useCommandPromptAsDefaultShell(currentProcess, configurationService) - .catch(ex => Logger.error('Use Command Prompt as default shell', ex)); - } - } + diagnostic, + invoke: async (): Promise<void> => { + sendTelemetryEvent(EventName.DIAGNOSTICS_ACTION, undefined, { + action: 'switchToCommandPrompt', + }); + useCommandPromptAsDefaultShell(currentProcess, configurationService).catch((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', - command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://aka.ms/CondaPwsh' }) - } + prompt: Common.moreInfo, + command: commandFactory.createCommand(diagnostic, { + type: 'launch', + 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<MessageCommandPrompt>, + @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<IDiagnostic[]> { + if (!(await this.shouldShowPrompt())) { + return []; + } + + return [new PylanceDefaultDiagnostic(Diagnostics.pylanceDefaultMessage, resource)]; + } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise<void> { + 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<boolean> { + 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 dcff919abf7e..9167e232a417 100644 --- a/src/client/application/diagnostics/checks/pythonInterpreter.ts +++ b/src/client/application/diagnostics/checks/pythonInterpreter.ts @@ -1,152 +1,326 @@ // 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 { DiagnosticSeverity, l10n } from 'vscode'; import '../../../common/extensions'; -import { IPlatformService } from '../../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry } from '../../../common/types'; -import { IInterpreterHelper, IInterpreterService, InterpreterType } from '../../../interpreter/contracts'; +import * as path from 'path'; +import { IConfigurationService, IDisposableRegistry, IInterpreterPathService, Resource } from '../../../common/types'; +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, IDiagnosticCommand, IDiagnosticHandlerService } from '../types'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticHandlerService, + 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.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.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) { - super(code, messages[code], DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder); + constructor( + code: DiagnosticCodes.NoPythonInterpretersDiagnostic | DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + resource: Resource, + workspaceService: IWorkspaceService, + scope = DiagnosticScope.WorkspaceFolder, + ) { + 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 { - protected changeThrottleTimeout = 1000; - private timeOut?: NodeJS.Timer; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { +export class InvalidPythonInterpreterService extends BaseDiagnosticsService + implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + ) { super( [ DiagnosticCodes.NoPythonInterpretersDiagnostic, - DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, - DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic, - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic - ], serviceContainer); - this.addPythonPathChangedHandler(); + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + DiagnosticCodes.InvalidComspecDiagnostic, + DiagnosticCodes.IncompletePathVarDiagnostic, + DiagnosticCodes.DefaultShellErrorDiagnostic, + ], + serviceContainer, + disposableRegistry, + false, + ); } - public async diagnose(): Promise<IDiagnostic[]> { - const configurationService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); - const settings = configurationService.getSettings(); - if (settings.disableInstallationChecks === true) { - return []; - } + public async activate(): Promise<void> { + const commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.TriggerEnvironmentSelection, (resource: Resource) => + this.triggerEnvSelectionIfNecessary(resource), + ), + ); const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService); - const hasInterpreters = await interpreterService.hasInterpreters; + this.disposableRegistry.push( + interpreterService.onDidChangeInterpreterConfiguration((e) => + commandManager.executeCommand(Commands.TriggerEnvironmentSelection, e).then(noop, noop), + ), + ); + } - if (!hasInterpreters) { - return [new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.NoPythonInterpretersDiagnostic)]; + public async diagnose(resource: Resource): Promise<IDiagnostic[]> { + return this.diagnoseDefaultShell(resource); + } + + public async _manualDiagnose(resource: Resource): Promise<IDiagnostic[]> { + const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); + const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService); + const diagnostics = await this.diagnoseDefaultShell(resource); + if (diagnostics.length > 0) { + return diagnostics; + } + const hasInterpreters = await interpreterService.hasInterpreters(); + const interpreterPathService = this.serviceContainer.get<IInterpreterPathService>(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(); + const currentInterpreter = await interpreterService.getActiveInterpreter(resource); if (!currentInterpreter) { - return [new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic)]; + if (useEnvExtension()) { + traceWarn(Interpreters.envExtNoActiveEnvironment); + } + return [ + new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + resource, + workspaceService, + ), + ]; } + return []; + } - const platform = this.serviceContainer.get<IPlatformService>(IPlatformService); - if (!platform.isMac) { - return []; + public async triggerEnvSelectionIfNecessary(resource: Resource): Promise<boolean> { + const diagnostics = await this._manualDiagnose(resource); + if (!diagnostics.length) { + return true; } + this.handle(diagnostics).ignoreErrors(); + return false; + } - const helper = this.serviceContainer.get<IInterpreterHelper>(IInterpreterHelper); - if (!helper.isMacDefaultPythonPath(settings.pythonPath)) { + private async diagnoseDefaultShell(resource: Resource): Promise<IDiagnostic[]> { + if (getOSType() !== OSType.Windows) { return []; } - if (!currentInterpreter || currentInterpreter.type !== InterpreterType.Unknown) { + const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService); + const currentInterpreter = await interpreterService.getActiveInterpreter(resource); + if (currentInterpreter) { return []; } - const interpreters = await interpreterService.getInterpreters(); - if (interpreters.filter(i => !helper.isMacDefaultPythonPath(i.path)).length === 0) { - return [new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic)]; + 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 []; + } - return [new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic)]; + private async isComspecInvalid() { + const comSpec = getEnvironmentVariable('ComSpec') ?? ''; + const fs = this.serviceContainer.get<IFileSystem>(IFileSystem); + return fs.fileExists(comSpec).then((exists) => !exists); } - public async handle(diagnostics: IDiagnostic[]): Promise<void> { - if (diagnostics.length === 0) { - return; - } - const messageService = this.serviceContainer.get<IDiagnosticHandlerService<MessageCommandPrompt>>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerServiceId); - await Promise.all(diagnostics.map(async diagnostic => { - if (!this.canHandle(diagnostic)) { - return; + + // 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; } - const commandPrompts = this.getCommandPrompts(diagnostic); - return messageService.handle(diagnostic, { commandPrompts, message: diagnostic.message }); - })); + } + return true; } - protected addPythonPathChangedHandler() { - const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - const disposables = this.serviceContainer.get<IDisposableRegistry>(IDisposableRegistry); - disposables.push(workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + + @cache(-1, true) + // eslint-disable-next-line class-methods-use-this + private async shellExecPython() { + const configurationService = this.serviceContainer.get<IConfigurationService>(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>(IProcessServiceFactory); + const service = await processServiceFactory.create(); + return service.shellExec(quoted, { timeout: 15000 }); } - protected async onDidChangeConfiguration(event: ConfigurationChangeEvent) { - const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - const workspacesUris: (Uri | undefined)[] = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders!.map(workspace => workspace.uri) : [undefined]; - if (workspacesUris.findIndex(uri => event.affectsConfiguration('python.pythonPath', uri)) === -1) { + + @cache(1000, true) // This is to handle throttling of multiple events. + protected async onHandle(diagnostics: IDiagnostic[]): Promise<void> { + if (diagnostics.length === 0) { return; } - // Lets wait, for more changes, dirty simple throttling. - if (this.timeOut) { - clearTimeout(this.timeOut); - this.timeOut = undefined; - } - this.timeOut = setTimeout(() => { - this.timeOut = undefined; - this.diagnose().then(dianostics => this.handle(dianostics)).ignoreErrors(); - }, this.changeThrottleTimeout); + const messageService = this.serviceContainer.get<IDiagnosticHandlerService<MessageCommandPrompt>>( + IDiagnosticHandlerService, + DiagnosticCommandPromptHandlerServiceId, + ); + await Promise.all( + diagnostics.map(async (diagnostic) => { + if (!this.canHandle(diagnostic)) { + return; + } + const commandPrompts = this.getCommandPrompts(diagnostic); + 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>(IDiagnosticsCommandFactory); - switch (diagnostic.code) { - case DiagnosticCodes.NoPythonInterpretersDiagnostic: { - return [{ - prompt: 'Download', - command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://www.python.org/downloads' }) - }]; - } - case DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic: - case DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic: { - return [{ - prompt: 'Select Python Interpreter', - command: commandFactory.createCommand(diagnostic, { type: 'executeVSCCommand', options: 'python.setInterpreter' }) - }]; - } - case DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic: { - return [{ - prompt: 'Learn more', - command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites' }) - }, + if ( + diagnostic.code === DiagnosticCodes.InvalidComspecDiagnostic || + diagnostic.code === DiagnosticCodes.IncompletePathVarDiagnostic || + diagnostic.code === DiagnosticCodes.DefaultShellErrorDiagnostic + ) { + const links: Record<DefaultShellDiagnostics, string> = { + InvalidComspecDiagnostic: 'https://aka.ms/AAk3djo', + IncompletePathVarDiagnostic: 'https://aka.ms/AAk744c', + DefaultShellErrorDiagnostic: 'https://aka.ms/AAk7qix', + }; + return [ { - prompt: 'Download', - command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://www.python.org/downloads' }) - }]; - } - default: { - throw new Error('Invalid diagnostic for \'InvalidPythonInterpreterService\''); - } + 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; + } +} + +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/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<MessageCommandPrompt>, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + ) { + super([DiagnosticCodes.JediPython27NotSupportedDiagnostic], serviceContainer, disposableRegistry, true, true); + } + + public diagnose(resource: Resource): Promise<IDiagnostic[]> { + let changed = false; + const config = this.workspaceService.getConfiguration('python'); + const value = config.inspect<string>('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<void> { + 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/commands/base.ts b/src/client/application/diagnostics/commands/base.ts index 8bbb4cc5f1e4..66a734d3fa93 100644 --- a/src/client/application/diagnostics/commands/base.ts +++ b/src/client/application/diagnostics/commands/base.ts @@ -6,7 +6,6 @@ import { IDiagnostic, IDiagnosticCommand } from '../types'; export abstract class BaseDiagnosticCommand implements IDiagnosticCommand { - constructor(public readonly diagnostic: IDiagnostic) { - } + constructor(public readonly diagnostic: IDiagnostic) {} public abstract invoke(): Promise<void>; } diff --git a/src/client/application/diagnostics/commands/execVSCCommand.ts b/src/client/application/diagnostics/commands/execVSCCommand.ts index b10fc001e5fc..50c7367f199a 100644 --- a/src/client/application/diagnostics/commands/execVSCCommand.ts +++ b/src/client/application/diagnostics/commands/execVSCCommand.ts @@ -3,19 +3,24 @@ 'use strict'; +import { CommandsWithoutArgs } from '../../../common/application/commands'; import { ICommandManager } from '../../../common/application/types'; import { IServiceContainer } from '../../../ioc/types'; import { sendTelemetryEvent } from '../../../telemetry'; -import { DIAGNOSTICS_ACTION } from '../../../telemetry/constants'; +import { EventName } from '../../../telemetry/constants'; import { IDiagnostic } from '../types'; import { BaseDiagnosticCommand } from './base'; export class ExecuteVSCCommand extends BaseDiagnosticCommand { - constructor(diagnostic: IDiagnostic, private serviceContainer: IServiceContainer, private commandName: string) { + constructor( + diagnostic: IDiagnostic, + private serviceContainer: IServiceContainer, + private commandName: CommandsWithoutArgs, + ) { super(diagnostic); } public async invoke(): Promise<void> { - sendTelemetryEvent(DIAGNOSTICS_ACTION, undefined, { commandName: this.commandName }); + sendTelemetryEvent(EventName.DIAGNOSTICS_ACTION, undefined, { commandName: this.commandName }); const cmdManager = this.serviceContainer.get<ICommandManager>(ICommandManager); return cmdManager.executeCommand(this.commandName).then(() => undefined); } diff --git a/src/client/application/diagnostics/commands/factory.ts b/src/client/application/diagnostics/commands/factory.ts index d4cc51fa84fe..b9bf14305703 100644 --- a/src/client/application/diagnostics/commands/factory.ts +++ b/src/client/application/diagnostics/commands/factory.ts @@ -13,8 +13,8 @@ import { CommandOptions, IDiagnosticsCommandFactory } from './types'; @injectable() export class DiagnosticsCommandFactory implements IDiagnosticsCommandFactory { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } - public createCommand<T>(diagnostic: IDiagnostic, options: CommandOptions): IDiagnosticCommand { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + public createCommand(diagnostic: IDiagnostic, options: CommandOptions): IDiagnosticCommand { const commandType = options.type; switch (options.type) { case 'ignore': { diff --git a/src/client/application/diagnostics/commands/ignore.ts b/src/client/application/diagnostics/commands/ignore.ts index 89b32abfe220..311128195975 100644 --- a/src/client/application/diagnostics/commands/ignore.ts +++ b/src/client/application/diagnostics/commands/ignore.ts @@ -5,16 +5,20 @@ import { IServiceContainer } from '../../../ioc/types'; import { sendTelemetryEvent } from '../../../telemetry'; -import { DIAGNOSTICS_ACTION } from '../../../telemetry/constants'; +import { EventName } from '../../../telemetry/constants'; import { DiagnosticScope, IDiagnostic, IDiagnosticFilterService } from '../types'; import { BaseDiagnosticCommand } from './base'; export class IgnoreDiagnosticCommand extends BaseDiagnosticCommand { - constructor(diagnostic: IDiagnostic, private serviceContainer: IServiceContainer, private readonly scope: DiagnosticScope) { + constructor( + diagnostic: IDiagnostic, + private serviceContainer: IServiceContainer, + private readonly scope: DiagnosticScope, + ) { super(diagnostic); } public invoke(): Promise<void> { - sendTelemetryEvent(DIAGNOSTICS_ACTION, undefined, { ignoreCode: this.diagnostic.code }); + sendTelemetryEvent(EventName.DIAGNOSTICS_ACTION, undefined, { ignoreCode: this.diagnostic.code }); const filter = this.serviceContainer.get<IDiagnosticFilterService>(IDiagnosticFilterService); return filter.ignoreDiagnostic(this.diagnostic.code, this.scope); } diff --git a/src/client/application/diagnostics/commands/launchBrowser.ts b/src/client/application/diagnostics/commands/launchBrowser.ts index 0fcd975bd48f..4509044f6770 100644 --- a/src/client/application/diagnostics/commands/launchBrowser.ts +++ b/src/client/application/diagnostics/commands/launchBrowser.ts @@ -6,7 +6,7 @@ import { IBrowserService } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { sendTelemetryEvent } from '../../../telemetry'; -import { DIAGNOSTICS_ACTION } from '../../../telemetry/constants'; +import { EventName } from '../../../telemetry/constants'; import { IDiagnostic } from '../types'; import { BaseDiagnosticCommand } from './base'; @@ -15,7 +15,7 @@ export class LaunchBrowserCommand extends BaseDiagnosticCommand { super(diagnostic); } public async invoke(): Promise<void> { - sendTelemetryEvent(DIAGNOSTICS_ACTION, undefined, { url: this.url }); + sendTelemetryEvent(EventName.DIAGNOSTICS_ACTION, undefined, { url: this.url }); const browser = this.serviceContainer.get<IBrowserService>(IBrowserService); return browser.launch(this.url); } diff --git a/src/client/application/diagnostics/commands/types.ts b/src/client/application/diagnostics/commands/types.ts index e22fbb0a2d9d..f65460b0d113 100644 --- a/src/client/application/diagnostics/commands/types.ts +++ b/src/client/application/diagnostics/commands/types.ts @@ -3,16 +3,17 @@ 'use strict'; +import { CommandsWithoutArgs } from '../../../common/application/commands'; import { DiagnosticScope, IDiagnostic, IDiagnosticCommand } from '../types'; export type CommandOption<Type, Option> = { type: Type; options: Option }; -export type LaunchBrowserOption = CommandOption<'launch', string>; -export type IgnoreDiagnostOption = CommandOption<'ignore', DiagnosticScope>; -export type ExecuteVSCCommandOption = CommandOption<'executeVSCCommand', string>; -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'); export interface IDiagnosticsCommandFactory { - createCommand<T>(diagnostic: IDiagnostic, options: CommandOptions): IDiagnosticCommand; + createCommand(diagnostic: IDiagnostic, options: CommandOptions): IDiagnosticCommand; } diff --git a/src/client/application/diagnostics/constants.ts b/src/client/application/diagnostics/constants.ts index 402ba94f4236..ca2867fc4f49 100644 --- a/src/client/application/diagnostics/constants.ts +++ b/src/client/application/diagnostics/constants.ts @@ -7,10 +7,21 @@ export enum DiagnosticCodes { InvalidEnvironmentPathVariableDiagnostic = 'InvalidEnvironmentPathVariableDiagnostic', InvalidDebuggerTypeDiagnostic = 'InvalidDebuggerTypeDiagnostic', NoPythonInterpretersDiagnostic = 'NoPythonInterpretersDiagnostic', - MacInterpreterSelectedAndNoOtherInterpretersDiagnostic = 'MacInterpreterSelectedAndNoOtherInterpretersDiagnostic', - MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic = 'MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic', - InvalidPythonPathInDebuggerDiagnostic = 'InvalidPythonPathInDebuggerDiagnostic', + MacInterpreterSelected = 'MacInterpreterSelected', + InvalidPythonPathInDebuggerSettingsDiagnostic = 'InvalidPythonPathInDebuggerSettingsDiagnostic', + InvalidPythonPathInDebuggerLaunchDiagnostic = 'InvalidPythonPathInDebuggerLaunchDiagnostic', EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic = 'EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic', - NoCurrentlySelectedPythonInterpreterDiagnostic = 'InvalidPythonInterpreterDiagnostic', - LSNotSupportedDiagnostic = 'LSNotSupportedDiagnostic' + InvalidPythonInterpreterDiagnostic = 'InvalidPythonInterpreterDiagnostic', + InvalidComspecDiagnostic = 'InvalidComspecDiagnostic', + IncompletePathVarDiagnostic = 'IncompletePathVarDiagnostic', + DefaultShellErrorDiagnostic = 'DefaultShellErrorDiagnostic', + LSNotSupportedDiagnostic = 'LSNotSupportedDiagnostic', + PythonPathDeprecatedDiagnostic = 'PythonPathDeprecatedDiagnostic', + JustMyCodeDiagnostic = 'JustMyCodeDiagnostic', + ConsoleTypeDiagnostic = 'ConsoleTypeDiagnostic', + ConfigPythonPathDiagnostic = 'ConfigPythonPathDiagnostic', + 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 908c02c61136..a304a6f558fc 100644 --- a/src/client/application/diagnostics/filter.ts +++ b/src/client/application/diagnostics/filter.ts @@ -10,25 +10,27 @@ import { DiagnosticScope, IDiagnosticFilterService } from './types'; export enum FilterKeys { GlobalDiagnosticFilter = 'GLOBAL_DIAGNOSTICS_FILTER', - WorkspaceDiagnosticFilter = 'WORKSPACE_DIAGNOSTICS_FILTER' + WorkspaceDiagnosticFilter = 'WORKSPACE_DIAGNOSTICS_FILTER', } @injectable() export class DiagnosticFilterService implements IDiagnosticFilterService { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - } + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} public async shouldIgnoreDiagnostic(code: string): Promise<boolean> { const factory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory); const globalState = factory.createGlobalPersistentState<string[]>(FilterKeys.GlobalDiagnosticFilter, []); - const workspaceState = factory.createWorkspacePersistentState<string[]>(FilterKeys.WorkspaceDiagnosticFilter, []); - return globalState.value.indexOf(code) >= 0 || - workspaceState.value.indexOf(code) >= 0; + const workspaceState = factory.createWorkspacePersistentState<string[]>( + FilterKeys.WorkspaceDiagnosticFilter, + [], + ); + return globalState.value.indexOf(code) >= 0 || workspaceState.value.indexOf(code) >= 0; } public async ignoreDiagnostic(code: string, scope: DiagnosticScope): Promise<void> { const factory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory); - const state = scope === DiagnosticScope.Global ? - factory.createGlobalPersistentState<string[]>(FilterKeys.GlobalDiagnosticFilter, []) : - factory.createWorkspacePersistentState<string[]>(FilterKeys.WorkspaceDiagnosticFilter, []); + const state = + scope === DiagnosticScope.Global + ? factory.createGlobalPersistentState<string[]>(FilterKeys.GlobalDiagnosticFilter, []) + : factory.createWorkspacePersistentState<string[]>(FilterKeys.WorkspaceDiagnosticFilter, []); const currentValue = state.value.slice(); await state.updateValue(currentValue.concat(code)); diff --git a/src/client/application/diagnostics/promptHandler.ts b/src/client/application/diagnostics/promptHandler.ts index 939f7074ec49..25b946b2ffb5 100644 --- a/src/client/application/diagnostics/promptHandler.ts +++ b/src/client/application/diagnostics/promptHandler.ts @@ -7,7 +7,7 @@ import { inject, injectable } from 'inversify'; import { DiagnosticSeverity } from 'vscode'; import { IApplicationShell } from '../../common/application/types'; import { IServiceContainer } from '../../ioc/types'; -import { IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService } from './types'; +import { IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService, IDiagnosticMessageOnCloseHandler } from './types'; export type MessageCommandPrompt = { commandPrompts: { @@ -15,6 +15,7 @@ export type MessageCommandPrompt = { command?: IDiagnosticCommand; }[]; message?: string; + onClose?: IDiagnosticMessageOnCloseHandler; }; export const DiagnosticCommandPromptHandlerServiceId = 'DiagnosticCommandPromptHandlerServiceId'; @@ -25,18 +26,32 @@ export class DiagnosticCommandPromptHandlerService implements IDiagnosticHandler constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { this.appShell = serviceContainer.get<IApplicationShell>(IApplicationShell); } - public async handle(diagnostic: IDiagnostic, options: MessageCommandPrompt = { commandPrompts: [] }): Promise<void> { - const prompts = options.commandPrompts.map(option => option.prompt); - const response = await this.displayMessage(options.message ? options.message : diagnostic.message, diagnostic.severity, prompts); + public async handle( + diagnostic: IDiagnostic, + options: MessageCommandPrompt = { commandPrompts: [] }, + ): Promise<void> { + const prompts = options.commandPrompts.map((option) => option.prompt); + const response = await this.displayMessage( + options.message ? options.message : diagnostic.message, + diagnostic.severity, + prompts, + ); + if (options.onClose) { + options.onClose(response); + } if (!response) { return; } - const selectedOption = options.commandPrompts.find(option => option.prompt === response); + const selectedOption = options.commandPrompts.find((option) => option.prompt === response); if (selectedOption && selectedOption.command) { await selectedOption.command.invoke(); } } - private async displayMessage(message: string, severity: DiagnosticSeverity, prompts: string[]): Promise<string | undefined> { + private async displayMessage( + message: string, + severity: DiagnosticSeverity, + prompts: string[], + ): Promise<string | undefined> { switch (severity) { case DiagnosticSeverity.Error: { return this.appShell.showErrorMessage(message, ...prompts); diff --git a/src/client/application/diagnostics/serviceRegistry.ts b/src/client/application/diagnostics/serviceRegistry.ts index 9cca48130258..acf460b88625 100644 --- a/src/client/application/diagnostics/serviceRegistry.ts +++ b/src/client/application/diagnostics/serviceRegistry.ts @@ -3,30 +3,101 @@ 'use strict'; +import { IExtensionSingleActivationService } from '../../activation/types'; import { IServiceManager } from '../../ioc/types'; import { IApplicationDiagnostics } from '../types'; import { ApplicationDiagnostics } from './applicationDiagnostics'; -import { EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId } from './checks/envPathVariable'; -import { InvalidDebuggerTypeDiagnosticsService, InvalidDebuggerTypeDiagnosticsServiceId } from './checks/invalidDebuggerType'; -import { InvalidPythonPathInDebuggerService, InvalidPythonPathInDebuggerServiceId } from './checks/invalidPythonPathInDebugger'; -import { LSNotSupportedDiagnosticService, LSNotSupportedDiagnosticServiceId } from './checks/lsNotSupported'; -import { PowerShellActivationHackDiagnosticsService, PowerShellActivationHackDiagnosticsServiceId } from './checks/powerShellActivation'; +import { + EnvironmentPathVariableDiagnosticsService, + EnvironmentPathVariableDiagnosticsServiceId, +} from './checks/envPathVariable'; +import { + InvalidPythonPathInDebuggerService, + InvalidPythonPathInDebuggerServiceId, +} from './checks/invalidPythonPathInDebugger'; +import { + JediPython27NotSupportedDiagnosticService, + JediPython27NotSupportedDiagnosticServiceId, +} from './checks/jediPython27NotSupported'; +import { + InvalidMacPythonInterpreterService, + InvalidMacPythonInterpreterServiceId, +} from './checks/macPythonInterpreter'; +import { + PowerShellActivationHackDiagnosticsService, + PowerShellActivationHackDiagnosticsServiceId, +} from './checks/powerShellActivation'; +import { PylanceDefaultDiagnosticService, PylanceDefaultDiagnosticServiceId } from './checks/pylanceDefault'; import { InvalidPythonInterpreterService, InvalidPythonInterpreterServiceId } from './checks/pythonInterpreter'; +import { + SwitchToDefaultLanguageServerDiagnosticService, + SwitchToDefaultLanguageServerDiagnosticServiceId, +} from './checks/switchToDefaultLS'; import { DiagnosticsCommandFactory } from './commands/factory'; import { IDiagnosticsCommandFactory } from './commands/types'; import { DiagnosticFilterService } from './filter'; -import { DiagnosticCommandPromptHandlerService, DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from './promptHandler'; +import { + DiagnosticCommandPromptHandlerService, + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt, +} from './promptHandler'; import { IDiagnosticFilterService, IDiagnosticHandlerService, IDiagnosticsService } from './types'; -export function registerTypes(serviceManager: IServiceManager) { +export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton<IDiagnosticFilterService>(IDiagnosticFilterService, DiagnosticFilterService); - serviceManager.addSingleton<IDiagnosticHandlerService<MessageCommandPrompt>>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerService, DiagnosticCommandPromptHandlerServiceId); - serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId); - serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, InvalidDebuggerTypeDiagnosticsService, InvalidDebuggerTypeDiagnosticsServiceId); - serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, InvalidPythonInterpreterService, InvalidPythonInterpreterServiceId); - serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, InvalidPythonPathInDebuggerService, InvalidPythonPathInDebuggerServiceId); - serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, LSNotSupportedDiagnosticService, LSNotSupportedDiagnosticServiceId); - serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, PowerShellActivationHackDiagnosticsService, PowerShellActivationHackDiagnosticsServiceId); + serviceManager.addSingleton<IDiagnosticHandlerService<MessageCommandPrompt>>( + IDiagnosticHandlerService, + DiagnosticCommandPromptHandlerService, + DiagnosticCommandPromptHandlerServiceId, + ); + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + EnvironmentPathVariableDiagnosticsService, + EnvironmentPathVariableDiagnosticsServiceId, + ); + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + InvalidPythonInterpreterService, + InvalidPythonInterpreterServiceId, + ); + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + InvalidPythonInterpreterService, + ); + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + InvalidPythonPathInDebuggerService, + InvalidPythonPathInDebuggerServiceId, + ); + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + PowerShellActivationHackDiagnosticsService, + PowerShellActivationHackDiagnosticsServiceId, + ); + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + InvalidMacPythonInterpreterService, + InvalidMacPythonInterpreterServiceId, + ); + + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + PylanceDefaultDiagnosticService, + PylanceDefaultDiagnosticServiceId, + ); + + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + JediPython27NotSupportedDiagnosticService, + JediPython27NotSupportedDiagnosticServiceId, + ); + + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + SwitchToDefaultLanguageServerDiagnosticService, + SwitchToDefaultLanguageServerDiagnosticServiceId, + ); + serviceManager.addSingleton<IDiagnosticsCommandFactory>(IDiagnosticsCommandFactory, DiagnosticsCommandFactory); serviceManager.addSingleton<IApplicationDiagnostics>(IApplicationDiagnostics, ApplicationDiagnostics); } diff --git a/src/client/application/diagnostics/surceMapSupportService.ts b/src/client/application/diagnostics/surceMapSupportService.ts deleted file mode 100644 index 5856aeb7e528..000000000000 --- a/src/client/application/diagnostics/surceMapSupportService.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 { 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<void> { - await this.configurationService.updateSetting('diagnostics.sourceMapsEnabled', true, undefined, ConfigurationTarget.Global); - await this.commandManager.executeCommand('workbench.action.reloadWindow'); - } - protected async onEnable(): Promise<void> { - 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 8daf816366c4..1dc9a3c689df 100644 --- a/src/client/application/diagnostics/types.ts +++ b/src/client/application/diagnostics/types.ts @@ -4,31 +4,35 @@ 'use strict'; import { DiagnosticSeverity, Uri } from 'vscode'; +import { Resource } from '../../common/types'; +import { PythonPathSource } from '../../debugger/extension/types'; +import { DiagnosticCodes } from './constants'; export enum DiagnosticScope { Global = 'Global', - WorkspaceFolder = 'WorkspaceFolder' + WorkspaceFolder = 'WorkspaceFolder', } export interface IDiagnostic { - readonly code: string; + readonly code: DiagnosticCodes; readonly message: string; readonly severity: DiagnosticSeverity; readonly scope: DiagnosticScope; + readonly resource: Resource; + readonly invokeHandler: 'always' | 'default'; + readonly shouldShowPrompt?: boolean; } export const IDiagnosticsService = Symbol('IDiagnosticsService'); export interface IDiagnosticsService { - diagnose(): Promise<IDiagnostic[]>; + readonly runInBackground: boolean; + readonly runInUntrustedWorkspace: boolean; + diagnose(resource: Resource): Promise<IDiagnostic[]>; canHandle(diagnostic: IDiagnostic): Promise<boolean>; handle(diagnostics: IDiagnostic[]): Promise<void>; } -export interface IInvalidPythonPathInDebuggerService extends IDiagnosticsService { - validatePythonPath(pythonPath?: string, resource?: Uri): Promise<boolean>; -} - export const IDiagnosticFilterService = Symbol('IDiagnosticFilterService'); export interface IDiagnosticFilterService { @@ -47,13 +51,16 @@ export interface IDiagnosticCommand { invoke(): Promise<void>; } +export type IDiagnosticMessageOnCloseHandler = (response?: string) => void; + +export const IInvalidPythonPathInSettings = Symbol('IInvalidPythonPathInSettings'); + +export interface IInvalidPythonPathInSettings extends IDiagnosticsService { + validateInterpreterPathInSettings(resource: Resource): Promise<boolean>; +} + export const IInvalidPythonPathInDebuggerService = Symbol('IInvalidPythonPathInDebuggerService'); export interface IInvalidPythonPathInDebuggerService extends IDiagnosticsService { - validatePythonPath(pythonPath?: string, resource?: Uri): Promise<boolean>; -} -export const ISourceMapSupportService = Symbol('ISourceMapSupportService'); -export interface ISourceMapSupportService { - register(): void; - enable(): Promise<void>; + validatePythonPath(pythonPath?: string, pythonPathSource?: PythonPathSource, resource?: Uri): Promise<boolean>; } diff --git a/src/client/application/serviceRegistry.ts b/src/client/application/serviceRegistry.ts index 38773bd20198..ff5376d70b24 100644 --- a/src/client/application/serviceRegistry.ts +++ b/src/client/application/serviceRegistry.ts @@ -5,10 +5,7 @@ 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) { - serviceManager.addSingleton<ISourceMapSupportService>(ISourceMapSupportService, SourceMapSupportService); diagnosticsRegisterTypes(serviceManager); } diff --git a/src/client/application/types.ts b/src/client/application/types.ts index 17344afbe8ab..cfd41f7b9746 100644 --- a/src/client/application/types.ts +++ b/src/client/application/types.ts @@ -3,15 +3,15 @@ 'use strict'; +import { Resource } from '../common/types'; + export const IApplicationDiagnostics = Symbol('IApplicationDiagnostics'); export interface IApplicationDiagnostics { /** * Perform pre-extension activation health checks. * E.g. validate user environment, etc. - * @returns {Promise<void>} - * @memberof IApplicationDiagnostics */ - performPreStartupHealthCheck(): Promise<void>; + performPreStartupHealthCheck(resource: Resource): Promise<void>; 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<void> => client.start(), + stop: (client: BaseLanguageClient): Promise<void> => 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<IBrowserExtensionApi> { + const reporter = getTelemetryReporter(); + + const activationPromise = Promise.resolve(buildApi(reporter)); + const pylanceExtension = vscode.extensions.getExtension<PylanceApi>(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<PylanceApi>(PYLANCE_EXTENSION_ID); + if (newPylanceExtension) { + changeDisposable.dispose(); + await runPylance(context, newPylanceExtension); + } + }); + + return activationPromise; +} + +export async function deactivate(): Promise<void> { + 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<PylanceApi>, +): Promise<void> { + 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<string, number> | 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<string, number> | 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<string, string> = {}; + 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<T>(extension: vscode.Extension<T>): Promise<vscode.Extension<T>> { + 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..d8e2e1d60d42 --- /dev/null +++ b/src/client/chat/baseTool.ts @@ -0,0 +1,80 @@ +// 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'; +import { StopWatch } from '../common/utils/stopWatch'; + +export abstract class BaseTool<T extends IResourceReference> implements LanguageModelTool<T> { + protected extraTelemetryProperties: Record<string, string> = {}; + constructor(private readonly toolName: string) {} + + async invoke( + options: LanguageModelToolInvocationOptions<T>, + token: CancellationToken, + ): Promise<LanguageModelToolResult> { + if (!workspace.isTrusted) { + return new LanguageModelToolResult([ + new LanguageModelTextPart('Cannot use this tool in an untrusted workspace.'), + ]); + } + this.extraTelemetryProperties = {}; + let error: Error | undefined; + const resource = resolveFilePath(options.input.resourcePath); + const stopWatch = new StopWatch(); + 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, stopWatch.elapsedTime, { + toolName: this.toolName, + failed, + failureCategory, + ...this.extraTelemetryProperties, + }); + } + } + protected abstract invokeImpl( + options: LanguageModelToolInvocationOptions<T>, + resource: Uri | undefined, + token: CancellationToken, + ): Promise<LanguageModelToolResult>; + + async prepareInvocation( + options: LanguageModelToolInvocationPrepareOptions<T>, + token: CancellationToken, + ): Promise<PreparedToolInvocation> { + const resource = resolveFilePath(options.input.resourcePath); + return this.prepareInvocationImpl(options, resource, token); + } + + protected abstract prepareInvocationImpl( + options: LanguageModelToolInvocationPrepareOptions<T>, + resource: Uri | undefined, + token: CancellationToken, + ): Promise<PreparedToolInvocation>; +} diff --git a/src/client/chat/configurePythonEnvTool.ts b/src/client/chat/configurePythonEnvTool.ts new file mode 100644 index 000000000000..914a92f81c52 --- /dev/null +++ b/src/client/chat/configurePythonEnvTool.ts @@ -0,0 +1,131 @@ +// 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, + getEnvTypeForTelemetry, + 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<IResourceReference> + implements LanguageModelTool<IResourceReference> { + 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<TerminalCodeExecutionProvider>( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get<ITerminalHelper>(ITerminalHelper); + this.recommendedEnvService = this.serviceContainer.get<IRecommendedEnvironmentService>( + IRecommendedEnvironmentService, + ); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions<IResourceReference>, + resource: Uri | undefined, + token: CancellationToken, + ): Promise<LanguageModelToolResult> { + const notebookResponse = getToolResponseIfNotebook(resource); + if (notebookResponse) { + this.extraTelemetryProperties.resolveOutcome = 'notebook'; + return notebookResponse; + } + + const workspaceSpecificEnv = await raceCancellationError( + this.hasAlreadyGotAWorkspaceSpecificEnvironment(resource), + token, + ); + + if (workspaceSpecificEnv) { + this.extraTelemetryProperties.resolveOutcome = 'existingWorkspaceEnv'; + this.extraTelemetryProperties.envType = getEnvTypeForTelemetry(workspaceSpecificEnv); + return getEnvDetailsForResponse( + workspaceSpecificEnv, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } + + if (await this.createEnvTool.shouldCreateNewVirtualEnv(resource, token)) { + try { + const result = await lm.invokeTool(CreateVirtualEnvTool.toolName, options, token); + this.extraTelemetryProperties.resolveOutcome = 'createdVirtualEnv'; + return result; + } 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. + this.extraTelemetryProperties.resolveOutcome = 'selectedEnvAfterCancelledCreate'; + return lm.invokeTool(SelectPythonEnvTool.toolName, { ...options, input }, token); + } + throw ex; + } + } else { + const input: ISelectPythonEnvToolArguments = { + ...options.input, + }; + this.extraTelemetryProperties.resolveOutcome = 'selectedEnv'; + return lm.invokeTool(SelectPythonEnvTool.toolName, { ...options, input }, token); + } + } + + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions<IResourceReference>, + _resource: Uri | undefined, + _token: CancellationToken, + ): Promise<PreparedToolInvocation> { + 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<ICreateVirtualEnvToolParams> + implements LanguageModelTool<ICreateVirtualEnvToolParams> { + 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<TerminalCodeExecutionProvider>( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get<ITerminalHelper>(ITerminalHelper); + this.recommendedEnvService = this.serviceContainer.get<IRecommendedEnvironmentService>( + IRecommendedEnvironmentService, + ); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions<ICreateVirtualEnvToolParams>, + resource: Uri | undefined, + token: CancellationToken, + ): Promise<LanguageModelToolResult> { + 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>(IInterpreterPathService); + const disposables = new DisposableStore(); + try { + disposables.add(hideEnvCreation()); + const interpreterChanged = new Promise<void>((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<boolean> { + 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<ICreateVirtualEnvToolParams>, + resource: Uri | undefined, + token: CancellationToken, + ): Promise<PreparedToolInvocation> { + 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..38dabce644a7 --- /dev/null +++ b/src/client/chat/getExecutableTool.ts @@ -0,0 +1,90 @@ +// 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, + getEnvTypeForTelemetry, + 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<IResourceReference> implements LanguageModelTool<IResourceReference> { + 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<TerminalCodeExecutionProvider>( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get<ITerminalHelper>(ITerminalHelper); + } + async invokeImpl( + _options: LanguageModelToolInvocationOptions<IResourceReference>, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise<LanguageModelToolResult> { + const notebookResponse = getToolResponseIfNotebook(resourcePath); + if (notebookResponse) { + return notebookResponse; + } + + const envPath = this.api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); + if (environment) { + this.extraTelemetryProperties.envType = getEnvTypeForTelemetry(environment); + } + + const message = await getEnvironmentDetails( + resourcePath, + this.api, + this.terminalExecutionService, + this.terminalHelper, + undefined, + token, + ); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); + } + + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions<IResourceReference>, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise<PreparedToolInvocation> { + 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..d25d72baeba8 --- /dev/null +++ b/src/client/chat/getPythonEnvTool.ts @@ -0,0 +1,131 @@ +// 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, + getEnvTypeForTelemetry, + 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<IResourceReference> + implements LanguageModelTool<IResourceReference> { + 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<TerminalCodeExecutionProvider>( + ICodeExecutionService, + 'standard', + ); + this.pythonExecFactory = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); + this.processServiceFactory = this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory); + this.terminalHelper = this.serviceContainer.get<ITerminalHelper>(ITerminalHelper); + } + + async invokeImpl( + _options: LanguageModelToolInvocationOptions<IResourceReference>, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise<LanguageModelToolResult> { + 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', + ); + } + this.extraTelemetryProperties.envType = getEnvTypeForTelemetry(environment); + + let packages = ''; + let responsePackageCount = 0; + if (useEnvExtension()) { + const api = await getEnvExtApi(); + const env = await api.getEnvironment(resourcePath); + const pkgs = env ? await api.getPackages(env) : []; + if (pkgs && pkgs.length > 0) { + responsePackageCount = pkgs.length; + // Installed Python packages, each in the format <name> or <name> (<version>). 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 <name> or <name> (<version>). 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, + ); + // Count lines starting with '- ' to get the number of packages + responsePackageCount = (packages.match(/^- /gm) || []).length; + } + this.extraTelemetryProperties.responsePackageCount = String(responsePackageCount); + const message = await getEnvironmentDetails( + resourcePath, + this.api, + this.terminalExecutionService, + this.terminalHelper, + packages, + token, + ); + + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); + } + + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions<IResourceReference>, + resourcePath: Uri | undefined, + _token: CancellationToken, + ): Promise<PreparedToolInvocation> { + 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..5d3d456361f9 --- /dev/null +++ b/src/client/chat/installPackagesTool.ts @@ -0,0 +1,167 @@ +// 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, + getEnvTypeForTelemetry, + 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<IInstallPackageArgs> + implements LanguageModelTool<IInstallPackageArgs> { + 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<IInstallPackageArgs>, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise<LanguageModelToolResult> { + const packageCount = options.input.packageList.length; + const packagePlurality = packageCount === 1 ? 'package' : 'packages'; + this.extraTelemetryProperties.packageCount = String(packageCount); + 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', + ); + } + this.extraTelemetryProperties.envType = getEnvTypeForTelemetry(environment); + const isConda = isCondaEnv(environment); + const installers = this.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller); + const installerType = isConda ? ModuleInstallerType.Conda : ModuleInstallerType.Pip; + this.extraTelemetryProperties.installerType = isConda ? 'conda' : '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<IInstallPackageArgs>, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise<PreparedToolInvocation> { + 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<string> { + 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 <name> or <name> (<version>). 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 <name> or <name> (<version>). 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<ISelectPythonEnvToolArguments> + implements LanguageModelTool<ISelectPythonEnvToolArguments> { + 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<TerminalCodeExecutionProvider>( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get<ITerminalHelper>(ITerminalHelper); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions<ISelectPythonEnvToolArguments>, + resource: Uri | undefined, + token: CancellationToken, + ): Promise<LanguageModelToolResult> { + 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<ISelectPythonEnvToolArguments>, + resource: Uri | undefined, + _token: CancellationToken, + ): Promise<PreparedToolInvocation> { + 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<boolean | undefined> { + 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>(IInterpreterPathService); + const interpreterChanged = new Promise<void>((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..2309316bcbdd --- /dev/null +++ b/src/client/chat/utils.ts @@ -0,0 +1,289 @@ +// 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<T>(promise: Promise<T>, token: CancellationToken): Promise<T> { + 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 function getEnvTypeForTelemetry(env: ResolvedEnvironment): string { + return (env.environment?.type || 'unknown').toLowerCase(); +} + +export async function getEnvironmentDetails( + resourcePath: Uri | undefined, + api: PythonExtension['environments'], + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, + packages: string | undefined, + token: CancellationToken, +): Promise<string> { + // 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<string> { + 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<LanguageModelToolResult> { + 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 new file mode 100644 index 000000000000..4230fb5de921 --- /dev/null +++ b/src/client/common/application/activeResource.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Resource } from '../types'; +import { IActiveResourceService, IDocumentManager, IWorkspaceService } from './types'; + +@injectable() +export class ActiveResourceService implements IActiveResourceService { + constructor( + @inject(IDocumentManager) private readonly documentManager: IDocumentManager, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + ) {} + + public getActiveResource(): Resource { + const editor = this.documentManager.activeTextEditor; + if (editor && !editor.document.isUntitled) { + return editor.document.uri; + } + return Array.isArray(this.workspaceService.workspaceFolders) && + this.workspaceService.workspaceFolders.length > 0 + ? this.workspaceService.workspaceFolders[0].uri + : undefined; + } +} diff --git a/src/client/common/application/applicationEnvironment.ts b/src/client/common/application/applicationEnvironment.ts index 3a49e699bccc..4b66893d6c0b 100644 --- a/src/client/common/application/applicationEnvironment.ts +++ b/src/client/common/application/applicationEnvironment.ts @@ -3,18 +3,59 @@ 'use strict'; -import { injectable } from 'inversify'; +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 { 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, + ) {} + + public get userSettingsFile(): string | undefined { + const vscodeFolderName = this.channel === 'insiders' ? 'Code - Insiders' : 'Code'; + switch (this.platform.osType) { + case OSType.OSX: + return path.join( + this.pathUtils.home, + 'Library', + 'Application Support', + vscodeFolderName, + 'User', + 'settings.json', + ); + case OSType.Linux: + return path.join(this.pathUtils.home, '.config', vscodeFolderName, 'User', 'settings.json'); + case OSType.Windows: + return this.process.env.APPDATA + ? path.join(this.process.env.APPDATA, vscodeFolderName, 'User', 'settings.json') + : undefined; + default: + return; + } + } public get appName(): string { return vscode.env.appName; } + public get vscodeVersion(): string { + return vscode.version; + } 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; } @@ -24,13 +65,40 @@ 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; } - // tslint:disable-next-line:no-any + + public get shell(): string { + return vscode.env.shell; + } + + public get onDidChangeShell(): vscode.Event<string> { + 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<string>().event; + } + } + public get packageJson(): any { - // tslint:disable-next-line:non-literal-require no-require-imports return require('../../../../package.json'); } + public get channel(): Channel { + return this.appName.indexOf('Insider') > 0 ? 'insiders' : 'stable'; + } + public get extensionChannel(): Channel { + const version = parse(this.packageJson.version); + // 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 ae4ff32cb017..8035d979efbd 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -2,19 +2,58 @@ // Licensed under the MIT License. 'use strict'; -// tslint:disable:no-require-imports no-var-requires no-any unified-signatures -const opn = require('opn'); - import { injectable } from 'inversify'; -import { CancellationToken, Disposable, InputBox, InputBoxOptions, MessageItem, MessageOptions, OpenDialogOptions, Progress, ProgressOptions, QuickPick, QuickPickItem, QuickPickOptions, SaveDialogOptions, StatusBarAlignment, StatusBarItem, Uri, window, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; -import { IApplicationShell } from './types'; +import { + CancellationToken, + CancellationTokenSource, + Disposable, + DocumentSelector, + env, + Event, + EventEmitter, + InputBox, + InputBoxOptions, + languages, + LanguageStatusItem, + LogOutputChannel, + MessageItem, + MessageOptions, + OpenDialogOptions, + Progress, + ProgressOptions, + QuickPick, + QuickPickItem, + QuickPickOptions, + SaveDialogOptions, + StatusBarAlignment, + StatusBarItem, + TextDocument, + TextEditor, + TreeView, + TreeViewOptions, + Uri, + ViewColumn, + window, + WindowState, + WorkspaceFolder, + WorkspaceFolderPickOptions, +} from 'vscode'; +import { traceError } from '../../logging'; +import { IApplicationShell, TerminalDataWriteEvent, TerminalExecutedCommand } from './types'; @injectable() export class ApplicationShell implements IApplicationShell { + public get onDidChangeWindowState(): Event<WindowState> { + return window.onDidChangeWindowState; + } public showInformationMessage(message: string, ...items: string[]): Thenable<string>; public showInformationMessage(message: string, options: MessageOptions, ...items: string[]): Thenable<string>; public showInformationMessage<T extends MessageItem>(message: string, ...items: T[]): Thenable<T>; - public showInformationMessage<T extends MessageItem>(message: string, options: MessageOptions, ...items: T[]): Thenable<T>; + public showInformationMessage<T extends MessageItem>( + message: string, + options: MessageOptions, + ...items: T[] + ): Thenable<T>; public showInformationMessage(message: string, options?: any, ...items: any[]): Thenable<any> { return window.showInformationMessage(message, options, ...items); } @@ -22,7 +61,11 @@ export class ApplicationShell implements IApplicationShell { public showWarningMessage(message: string, ...items: string[]): Thenable<string>; public showWarningMessage(message: string, options: MessageOptions, ...items: string[]): Thenable<string>; public showWarningMessage<T extends MessageItem>(message: string, ...items: T[]): Thenable<T>; - public showWarningMessage<T extends MessageItem>(message: string, options: MessageOptions, ...items: T[]): Thenable<T>; + public showWarningMessage<T extends MessageItem>( + message: string, + options: MessageOptions, + ...items: T[] + ): Thenable<T>; public showWarningMessage(message: any, options?: any, ...items: any[]) { return window.showWarningMessage(message, options, ...items); } @@ -30,13 +73,25 @@ export class ApplicationShell implements IApplicationShell { public showErrorMessage(message: string, ...items: string[]): Thenable<string>; public showErrorMessage(message: string, options: MessageOptions, ...items: string[]): Thenable<string>; public showErrorMessage<T extends MessageItem>(message: string, ...items: T[]): Thenable<T>; - public showErrorMessage<T extends MessageItem>(message: string, options: MessageOptions, ...items: T[]): Thenable<T>; + public showErrorMessage<T extends MessageItem>( + message: string, + options: MessageOptions, + ...items: T[] + ): Thenable<T>; public showErrorMessage(message: any, options?: any, ...items: any[]) { return window.showErrorMessage(message, options, ...items); } - public showQuickPick(items: string[] | Thenable<string[]>, options?: QuickPickOptions, token?: CancellationToken): Thenable<string>; - public showQuickPick<T extends QuickPickItem>(items: T[] | Thenable<T[]>, options?: QuickPickOptions, token?: CancellationToken): Thenable<T>; + public showQuickPick( + items: string[] | Thenable<string[]>, + options?: QuickPickOptions, + token?: CancellationToken, + ): Thenable<string>; + public showQuickPick<T extends QuickPickItem>( + items: T[] | Thenable<T[]>, + options?: QuickPickOptions, + token?: CancellationToken, + ): Thenable<T>; public showQuickPick(items: any, options?: any, token?: any): Thenable<any> { return window.showQuickPick(items, options, token); } @@ -50,8 +105,16 @@ export class ApplicationShell implements IApplicationShell { public showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable<string | undefined> { return window.showInputBox(options, token); } + public showTextDocument( + document: TextDocument, + column?: ViewColumn, + preserveFocus?: boolean, + ): Thenable<TextEditor> { + return window.showTextDocument(document, column, preserveFocus); + } + public openUrl(url: string): void { - opn(url); + env.openExternal(Uri.parse(url)); } public setStatusBarMessage(text: string, hideAfterTimeout: number): Disposable; @@ -61,19 +124,70 @@ 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<WorkspaceFolder | undefined> { return window.showWorkspaceFolderPick(options); } - public withProgress<R>(options: ProgressOptions, task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable<R>): Thenable<R> { + public withProgress<R>( + options: ProgressOptions, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable<R>, + ): Thenable<R> { return window.withProgress<R>(options, task); } + public withProgressCustomIcon<R>( + icon: string, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable<R>, + ): Thenable<R> { + 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) => { + statusBarProgress.dispose(); + return result; + }); + } public createQuickPick<T extends QuickPickItem>(): QuickPick<T> { return window.createQuickPick<T>(); } public createInputBox(): InputBox { return window.createInputBox(); } + public createTreeView<T>(viewId: string, options: TreeViewOptions<T>): TreeView<T> { + return window.createTreeView<T>(viewId, options); + } + 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<TerminalDataWriteEvent> { + try { + return window.onDidWriteTerminalData; + } catch (ex) { + traceError('Failed to get proposed API onDidWriteTerminalData', ex); + return new EventEmitter<TerminalDataWriteEvent>().event; + } + } + public get onDidExecuteTerminalCommand(): Event<TerminalExecutedCommand> | undefined { + try { + return window.onDidExecuteTerminalCommand; + } catch (ex) { + traceError('Failed to get proposed API TerminalExecutedCommand', ex); + return undefined; + } + } } diff --git a/src/client/common/application/clipboard.ts b/src/client/common/application/clipboard.ts new file mode 100644 index 000000000000..619d9ea60b1e --- /dev/null +++ b/src/client/common/application/clipboard.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { env } from 'vscode'; +import { IClipboard } from './types'; + +@injectable() +export class ClipboardService implements IClipboard { + public async readText(): Promise<string> { + return env.clipboard.readText(); + } + public async writeText(value: string): Promise<void> { + await env.clipboard.writeText(value); + } +} diff --git a/src/client/common/application/commandManager.ts b/src/client/common/application/commandManager.ts index af42ffe744b9..9e1f34a5885b 100644 --- a/src/client/common/application/commandManager.ts +++ b/src/client/common/application/commandManager.ts @@ -1,15 +1,13 @@ // 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'; import { ICommandManager } from './types'; @injectable() export class CommandManager implements ICommandManager { - /** * Registers a command that can be invoked via a keyboard shortcut, * a menu item, an action, or directly. @@ -22,8 +20,14 @@ export class CommandManager implements ICommandManager { * @param thisArg The `this` context used when invoking the handler function. * @return Disposable which unregisters this command on disposal. */ - public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { - return commands.registerCommand(command, callback, thisArg); + // 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); } /** @@ -40,7 +44,14 @@ export class CommandManager implements ICommandManager { * @param thisArg The `this` context used when invoking the handler function. * @return Disposable which unregisters this command on disposal. */ - public registerTextEditorCommand(command: string, callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, thisArg?: any): Disposable { + // 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, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any + thisArg?: any, + ): Disposable { return commands.registerTextEditorCommand(command, callback, thisArg); } @@ -58,7 +69,12 @@ 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. */ - public executeCommand<T>(command: string, ...rest: any[]): Thenable<T | undefined> { + // eslint-disable-next-line class-methods-use-this + public executeCommand< + T, + E extends keyof ICommandNameArgumentTypeMapping, + U extends ICommandNameArgumentTypeMapping[E] + >(command: E, ...rest: U): Thenable<T | undefined> { return commands.executeCommand<T>(command, ...rest); } @@ -69,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<string[]> { return commands.getCommands(filterInternal); } diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts new file mode 100644 index 000000000000..b43dc0a1e4a4 --- /dev/null +++ b/src/client/common/application/commands.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { CancellationToken, Position, TestItem, TextDocument, Uri } from 'vscode'; +import { Commands as LSCommands } from '../../activation/commands'; +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 { + [Commands.InstallPythonOnMac]: []; + [Commands.InstallJupyter]: []; + [Commands.InstallPythonOnLinux]: []; + [Commands.InstallPython]: []; + [Commands.ClearWorkspaceInterpreter]: []; + [Commands.Set_Interpreter]: []; + [Commands.Set_ShebangInterpreter]: []; + ['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']: []; + [Commands.ViewOutput]: []; + [Commands.Start_REPL]: []; + [Commands.Exec_Selection_In_Terminal]: []; + [Commands.Exec_Selection_In_Django_Shell]: []; + [Commands.Create_Terminal]: []; + [Commands.PickLocalProcess]: []; + [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 { + [Commands.CopyTestId]: [TestItem]; + [Commands.Create_Environment]: [CreateEnvironmentOptions]; + ['vscode.openWith']: [Uri, string]; + ['workbench.action.quickOpen']: [string]; + ['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.SelectAndInsertDebugConfiguration']: [TextDocument, Position, CancellationToken]; + ['vscode.open']: [Uri]; + ['notebook.execute']: []; + ['notebook.cell.execute']: []; + ['notebook.cell.insertCodeCellBelow']: []; + ['notebook.undo']: []; + ['notebook.redo']: []; + ['python.viewLanguageServerOutput']: []; + ['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.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.Debug_In_Terminal]: [Uri]; + [Commands.Tests_Configure]: [undefined, undefined | CommandSource, undefined | 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<void> { + this.disposables.push(this.commandManager.registerCommand(Commands.CreateNewFile, this.createPythonFile, this)); + } + + public async createPythonFile(): Promise<void> { + 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 new file mode 100644 index 000000000000..ebad15dbb70d --- /dev/null +++ b/src/client/common/application/commands/reloadCommand.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../../../activation/types'; +import { Common } from '../../utils/localize'; +import { noop } from '../../utils/misc'; +import { IApplicationShell, ICommandManager } from '../types'; + +/** + * Prompts user to reload VS Code with a custom message, and reloads if necessary. + */ +@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, + ) {} + public async activate(): Promise<void> { + 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) { + 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<void> { + 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<void> { + 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<string, unknown>; + 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]) : '"<placeholder>"'; + 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]) : '"<placeholder>"'; + 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<string>('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<ExtensionContextKey, boolean> = new Map(); + + constructor(@inject(ICommandManager) private readonly commandManager: ICommandManager) {} + + public async setContext(key: ExtensionContextKey, value: boolean): Promise<void> { + 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/debugService.ts b/src/client/common/application/debugService.ts index df1b6e508b8a..7de039e946c2 100644 --- a/src/client/common/application/debugService.ts +++ b/src/client/common/application/debugService.ts @@ -4,7 +4,20 @@ 'use strict'; import { injectable } from 'inversify'; -import { Breakpoint, BreakpointsChangeEvent, debug, DebugConfiguration, DebugConsole, DebugSession, DebugSessionCustomEvent, Disposable, Event, WorkspaceFolder } from 'vscode'; +import { + Breakpoint, + BreakpointsChangeEvent, + debug, + DebugAdapterDescriptorFactory, + DebugConfiguration, + DebugConsole, + DebugSession, + DebugSessionCustomEvent, + DebugSessionOptions, + Disposable, + Event, + WorkspaceFolder, +} from 'vscode'; import { IDebugService } from './types'; @injectable() @@ -16,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<DebugSession | undefined> { @@ -34,12 +47,20 @@ export class DebugService implements IDebugService { public get onDidChangeBreakpoints(): Event<BreakpointsChangeEvent> { return debug.onDidChangeBreakpoints; } - // tslint:disable-next-line:no-any + public registerDebugConfigurationProvider(debugType: string, provider: any): Disposable { return debug.registerDebugConfigurationProvider(debugType, provider); } - public startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration): Thenable<boolean> { - return debug.startDebugging(folder, nameOrConfiguration); + + public registerDebugAdapterTrackerFactory(debugType: string, provider: any): Disposable { + return debug.registerDebugAdapterTrackerFactory(debugType, provider); + } + public startDebugging( + folder: WorkspaceFolder | undefined, + nameOrConfiguration: string | DebugConfiguration, + parentSession?: DebugSession | DebugSessionOptions, + ): Thenable<boolean> { + return debug.startDebugging(folder, nameOrConfiguration, parentSession); } public addBreakpoints(breakpoints: Breakpoint[]): void { debug.addBreakpoints(breakpoints); @@ -47,4 +68,10 @@ export class DebugService implements IDebugService { public removeBreakpoints(breakpoints: Breakpoint[]): void { debug.removeBreakpoints(breakpoints); } + public registerDebugAdapterDescriptorFactory( + debugType: string, + factory: DebugAdapterDescriptorFactory, + ): Disposable { + return debug.registerDebugAdapterDescriptorFactory(debugType, factory); + } } diff --git a/src/client/common/application/documentManager.ts b/src/client/common/application/documentManager.ts index e4f1fa3dc225..617d335e402b 100644 --- a/src/client/common/application/documentManager.ts +++ b/src/client/common/application/documentManager.ts @@ -1,27 +1,44 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - -// tslint:disable:no-any unified-signatures - import { injectable } from 'inversify'; -import { Event, TextDocument, TextDocumentShowOptions, TextEditor, TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, TextEditorViewColumnChangeEvent, Uri, ViewColumn, window, workspace, WorkspaceEdit } from 'vscode'; +import { + DecorationRenderOptions, + Event, + TextDocument, + TextDocumentChangeEvent, + TextDocumentShowOptions, + TextEditor, + TextEditorDecorationType, + TextEditorOptionsChangeEvent, + TextEditorSelectionChangeEvent, + TextEditorViewColumnChangeEvent, + Uri, + ViewColumn, + window, + workspace, + WorkspaceEdit, +} from 'vscode'; + import { IDocumentManager } from './types'; @injectable() export class DocumentManager implements IDocumentManager { - public get textDocuments(): TextDocument[] { + public get textDocuments(): readonly TextDocument[] { return workspace.textDocuments; } public get activeTextEditor(): TextEditor | undefined { return window.activeTextEditor; } - public get visibleTextEditors(): TextEditor[] { + public get visibleTextEditors(): readonly TextEditor[] { return window.visibleTextEditors; } public get onDidChangeActiveTextEditor(): Event<TextEditor | undefined> { return window.onDidChangeActiveTextEditor; } - public get onDidChangeVisibleTextEditors(): Event<TextEditor[]> { + public get onDidChangeTextDocument(): Event<TextDocumentChangeEvent> { + return workspace.onDidChangeTextDocument; + } + public get onDidChangeVisibleTextEditors(): Event<readonly TextEditor[]> { return window.onDidChangeVisibleTextEditors; } public get onDidChangeTextEditorSelection(): Event<TextEditorSelectionChangeEvent> { @@ -57,4 +74,7 @@ export class DocumentManager implements IDocumentManager { public applyEdit(edit: WorkspaceEdit): Thenable<boolean> { return workspace.applyEdit(edit); } + public createTextEditorDecorationType(options: DecorationRenderOptions): TextEditorDecorationType { + return window.createTextEditorDecorationType(options); + } } diff --git a/src/client/common/application/extensions.ts b/src/client/common/application/extensions.ts index 4a98ffa9b3b0..e4b8f5bce73d 100644 --- a/src/client/common/application/extensions.ts +++ b/src/client/common/application/extensions.ts @@ -1,21 +1,100 @@ -// 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 { Extension, extensions } from 'vscode'; +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 - public get all(): Extension<any>[] { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _cachedExtensions?: readonly Extension<any>[]; + + constructor(@inject(IFileSystem) private readonly fs: IFileSystem) {} + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public get all(): readonly Extension<any>[] { return extensions.all; } - // tslint:disable-next-line:no-any - public getExtension(extensionId: any) { + public get onDidChange(): Event<void> { + return extensions.onDidChange; + } + + public getExtension(extensionId: string): Extension<unknown> | 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/languageService.ts b/src/client/common/application/languageService.ts new file mode 100644 index 000000000000..6cbdda85b417 --- /dev/null +++ b/src/client/common/application/languageService.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { CompletionItemProvider, DocumentSelector, languages } from 'vscode'; +import { Disposable } from 'vscode-jsonrpc'; +import { ILanguageService } from './types'; + +@injectable() +export class LanguageService implements ILanguageService { + public registerCompletionItemProvider( + selector: DocumentSelector, + provider: CompletionItemProvider, + ...triggerCharacters: string[] + ): Disposable { + return languages.registerCompletionItemProvider(selector, provider, ...triggerCharacters); + } +} 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<void> | 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<Terminal>(); + constructor() { + window.onDidOpenTerminal((terminal) => { + this.didOpenTerminal.fire(monkeyPatchTerminal(terminal)); + }); + } public get onDidCloseTerminal(): Event<Terminal> { return window.onDidCloseTerminal; } public get onDidOpenTerminal(): Event<Terminal> { - 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 f3e342226412..34a95fb604f0 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -1,22 +1,32 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; + import { Breakpoint, BreakpointsChangeEvent, CancellationToken, + CompletionItemProvider, ConfigurationChangeEvent, + DebugAdapterDescriptorFactory, + DebugAdapterTrackerFactory, DebugConfiguration, DebugConfigurationProvider, DebugConsole, DebugSession, DebugSessionCustomEvent, + DebugSessionOptions, + DecorationRenderOptions, Disposable, + DocumentSelector, Event, FileSystemWatcher, GlobPattern, InputBox, InputBoxOptions, + LanguageStatusItem, + LogOutputChannel, MessageItem, MessageOptions, OpenDialogOptions, @@ -30,26 +40,95 @@ import { StatusBarItem, Terminal, TerminalOptions, + TerminalShellExecutionEndEvent, + TerminalShellIntegrationChangeEvent, TextDocument, + TextDocumentChangeEvent, TextDocumentShowOptions, TextEditor, + TextEditorDecorationType, TextEditorEdit, TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, TextEditorViewColumnChangeEvent, + TreeView, + TreeViewOptions, + UIKind, Uri, ViewColumn, + WindowState, WorkspaceConfiguration, WorkspaceEdit, WorkspaceFolder, WorkspaceFolderPickOptions, - WorkspaceFoldersChangeEvent + WorkspaceFoldersChangeEvent, } from 'vscode'; -// tslint:disable:no-any unified-signatures +import { Channel } from '../constants'; +import { Resource } from '../types'; +import { ICommandNameArgumentTypeMapping } from './commands'; +import { ExtensionContextKey } from './contextKeys'; + +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<TerminalExecutedCommand> | 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<WindowState>; + + /** + * 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<TerminalDataWriteEvent>; + showInformationMessage(message: string, ...items: string[]): Thenable<string | undefined>; /** @@ -84,7 +163,11 @@ export interface IApplicationShell { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - showInformationMessage<T extends MessageItem>(message: string, options: MessageOptions, ...items: T[]): Thenable<T | undefined>; + showInformationMessage<T extends MessageItem>( + message: string, + options: MessageOptions, + ...items: T[] + ): Thenable<T | undefined>; /** * Show a warning message. @@ -130,7 +213,11 @@ export interface IApplicationShell { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - showWarningMessage<T extends MessageItem>(message: string, options: MessageOptions, ...items: T[]): Thenable<T | undefined>; + showWarningMessage<T extends MessageItem>( + message: string, + options: MessageOptions, + ...items: T[] + ): Thenable<T | undefined>; /** * Show an error message. @@ -176,7 +263,11 @@ export interface IApplicationShell { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - showErrorMessage<T extends MessageItem>(message: string, options: MessageOptions, ...items: T[]): Thenable<T | undefined>; + showErrorMessage<T extends MessageItem>( + message: string, + options: MessageOptions, + ...items: T[] + ): Thenable<T | undefined>; /** * Shows a selection list. @@ -186,7 +277,11 @@ export interface IApplicationShell { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selection or `undefined`. */ - showQuickPick(items: string[] | Thenable<string[]>, options?: QuickPickOptions, token?: CancellationToken): Thenable<string | undefined>; + showQuickPick( + items: string[] | Thenable<string[]>, + options?: QuickPickOptions, + token?: CancellationToken, + ): Thenable<string | undefined>; /** * Shows a selection list. @@ -196,7 +291,11 @@ export interface IApplicationShell { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected item or `undefined`. */ - showQuickPick<T extends QuickPickItem>(items: T[] | Thenable<T[]>, options?: QuickPickOptions, token?: CancellationToken): Thenable<T | undefined>; + showQuickPick<T extends QuickPickItem>( + items: T[] | Thenable<T[]>, + options?: QuickPickOptions, + token?: CancellationToken, + ): Thenable<T | undefined>; /** * Shows a file open dialog to the user which allows to select a file @@ -229,6 +328,19 @@ export interface IApplicationShell { */ showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable<string | undefined>; + /** + * 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<TextEditor>; + /** * Creates a [QuickPick](#QuickPick) to let the user pick an item from a list * of items of type T. @@ -276,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<any>): Disposable; /** @@ -297,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. @@ -326,13 +439,58 @@ export interface IApplicationShell { * * @return The thenable the task-callback returned. */ - withProgress<R>(options: ProgressOptions, task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable<R>): Thenable<R>; + withProgress<R>( + options: ProgressOptions, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable<R>, + ): Thenable<R>; + + /** + * Show progress in the status bar with a custom icon instead of the default spinner. + * Progress is shown while running the given callback and while the promise it returned isn't resolved nor rejected. + * At the moment, progress can only be displayed in the status bar when using this method. If you want to + * display it elsewhere, use `withProgress`. + * + * @param icon A valid Octicon. + * + * @param task A callback returning a promise. Progress state can be reported with + * the provided [progress](#Progress)-object. + * + * To report discrete progress, use `increment` to indicate how much work has been completed. Each call with + * a `increment` value will be summed up and reflected as overall progress until 100% is reached (a value of + * e.g. `10` accounts for `10%` of work done). + * Note that currently only `ProgressLocation.Notification` is capable of showing discrete progress. + * + * To monitor if the operation has been cancelled by the user, use the provided [`CancellationToken`](#CancellationToken). + * Note that currently only `ProgressLocation.Notification` is supporting to show a cancel button to cancel the + * long running operation. + * + * @return The thenable the task-callback returned. + */ + withProgressCustomIcon<R>( + icon: string, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable<R>, + ): Thenable<R>; + + /** + * Create a [TreeView](#TreeView) for the view contributed using the extension point `views`. + * @param viewId Id of the view contributed using the extension point `views`. + * @param options Options for creating the [TreeView](#TreeView) + * @returns a [TreeView](#TreeView). + */ + createTreeView<T>(viewId: string, options: TreeViewOptions<T>): TreeView<T>; + + /** + * Creates a new [output channel](#OutputChannel) with the given name. + * + * @param name Human-readable string which will be used to represent the channel in the UI. + */ + createOutputChannel(name: string): LogOutputChannel; + createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem; } export const ICommandManager = Symbol('ICommandManager'); export interface ICommandManager { - /** * Registers a command that can be invoked via a keyboard shortcut, * a menu item, an action, or directly. @@ -345,7 +503,13 @@ export interface ICommandManager { * @param thisArg The `this` context used when invoking the handler function. * @return Disposable which unregisters this command on disposal. */ - registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable; + registerCommand<E extends keyof ICommandNameArgumentTypeMapping, U extends ICommandNameArgumentTypeMapping[E]>( + command: E, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback: (...args: U) => any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + thisArg?: any, + ): Disposable; /** * Registers a text editor command that can be invoked via a keyboard shortcut, @@ -361,7 +525,13 @@ export interface ICommandManager { * @param thisArg The `this` context used when invoking the handler function. * @return Disposable which unregisters this command on disposal. */ - registerTextEditorCommand(command: string, callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, thisArg?: any): Disposable; + registerTextEditorCommand( + command: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + thisArg?: any, + ): Disposable; /** * Executes the command denoted by the given command identifier. @@ -377,7 +547,10 @@ export interface ICommandManager { * @return A thenable that resolves to the returned value of the given command. `undefined` when * the command handler function doesn't return anything. */ - executeCommand<T>(command: string, ...rest: any[]): Thenable<T | undefined>; + executeCommand<T, E extends keyof ICommandNameArgumentTypeMapping, U extends ICommandNameArgumentTypeMapping[E]>( + command: E, + ...rest: U + ): Thenable<T | undefined>; /** * Retrieve the list of all available commands. Commands starting an underscore are @@ -389,6 +562,16 @@ export interface ICommandManager { getCommands(filterInternal?: boolean): Thenable<string[]>; } +export const IContextKeyManager = Symbol('IContextKeyManager'); +export interface IContextKeyManager { + setContext(key: ExtensionContextKey, value: boolean): Promise<void>; +} + +export const IJupyterExtensionDependencyManager = Symbol('IJupyterExtensionDependencyManager'); +export interface IJupyterExtensionDependencyManager { + readonly isJupyterExtensionInstalled: boolean; +} + export const IDocumentManager = Symbol('IDocumentManager'); export interface IDocumentManager { @@ -397,7 +580,7 @@ export interface IDocumentManager { * * @readonly */ - readonly textDocuments: TextDocument[]; + readonly textDocuments: readonly TextDocument[]; /** * The currently active editor or `undefined`. The active editor is the one * that currently has focus or, when none has focus, the one that has changed @@ -408,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) @@ -417,11 +600,18 @@ export interface IDocumentManager { */ readonly onDidChangeActiveTextEditor: Event<TextEditor | undefined>; + /** + * An event that is emitted when a [text document](#TextDocument) is changed. This usually happens + * when the [contents](#TextDocument.getText) changes but also when other things like the + * [dirty](#TextDocument.isDirty)-state changes. + */ + readonly onDidChangeTextDocument: Event<TextDocumentChangeEvent>; + /** * An [event](#Event) which fires when the array of [visible editors](#window.visibleTextEditors) * has changed. */ - readonly onDidChangeVisibleTextEditors: Event<TextEditor[]>; + readonly onDidChangeVisibleTextEditors: Event<readonly TextEditor[]>; /** * An [event](#Event) which fires when the selection in an editor has changed. @@ -533,6 +723,14 @@ export interface IDocumentManager { * @return A thenable that resolves when the edit could be applied. */ applyEdit(edit: WorkspaceEdit): Thenable<boolean>; + + /** + * Create a TextEditorDecorationType that can be used to add decorations to text editors. + * + * @param options Rendering options for the decoration type. + * @return A new decoration type instance. + */ + createTextEditorDecorationType(options: DecorationRenderOptions): TextEditorDecorationType; } export const IWorkspaceService = Symbol('IWorkspaceService'); @@ -546,13 +744,54 @@ 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<void>; + /** * List of workspace folders or `undefined` when no folder is open. * *Note* that the first entry corresponds to the value of `rootPath`. * * @readonly */ - readonly workspaceFolders: WorkspaceFolder[] | undefined; + readonly workspaceFolders: readonly WorkspaceFolder[] | undefined; + + /** + * The location of the workspace file, for example: + * + * `file:///Users/name/Development/myProject.code-workspace` + * + * or + * + * `untitled:1555503116870` + * + * for a workspace that is untitled and not yet saved. + * + * Depending on the workspace that is opened, the value will be: + * * `undefined` when no workspace or a single folder is opened + * * the path of the workspace file as `Uri` otherwise. if the workspace + * is untitled, the returned URI will use the `untitled:` scheme + * + * The location can e.g. be used with the `vscode.openFolder` command to + * open the workspace again after it has been closed. + * + * **Example:** + * ```typescript + * vscode.commands.executeCommand('vscode.openFolder', uriOfWorkspace); + * ``` + * + * **Note:** it is not advised to use `workspace.workspaceFile` to write + * configuration data into the file. You can use `workspace.getConfiguration().update()` + * for that purpose which will work both when a single folder is opened as + * well as an untitled or saved workspace. + */ + readonly workspaceFile: Resource; /** * An event that is emitted when a workspace folder is added or removed. @@ -564,12 +803,9 @@ export interface IWorkspaceService { */ readonly onDidChangeConfiguration: Event<ConfigurationChangeEvent>; /** - * 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 @@ -578,15 +814,12 @@ export interface IWorkspaceService { * @param uri An uri. * @return A workspace folder or `undefined` */ - getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined; + getWorkspaceFolder(uri: Resource): WorkspaceFolder | undefined; /** * 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): string; + getWorkspaceFolderIdentifier(resource: Uri | undefined, defaultValue?: string): string; /** * Returns a path that is relative to the workspace folder or folders. * @@ -616,7 +849,12 @@ export interface IWorkspaceService { * @param ignoreDeleteEvents Ignore when files have been deleted. * @return A new file system watcher instance. */ - createFileSystemWatcher(globPattern: GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): FileSystemWatcher; + createFileSystemWatcher( + globPattern: GlobPattern, + ignoreCreateEvents?: boolean, + ignoreChangeEvents?: boolean, + ignoreDeleteEvents?: boolean, + ): FileSystemWatcher; /** * Find files across all [workspace folders](#workspace.workspaceFolders) in the workspace. @@ -626,13 +864,19 @@ 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 * [workspace folders](#workspace.workspaceFolders) are opened. */ - findFiles(include: GlobPattern, exclude?: GlobPattern, maxResults?: number, token?: CancellationToken): Thenable<Uri[]>; + findFiles( + include: GlobPattern, + exclude?: GlobPattern, + maxResults?: number, + token?: CancellationToken, + ): Thenable<Uri[]>; /** * Get a workspace configuration object. @@ -645,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<TextDocument>; + /** + * 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<Uri | undefined>; } export const ITerminalManager = Symbol('ITerminalManager'); @@ -670,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'); @@ -690,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) @@ -729,6 +1000,26 @@ export interface IDebugService { */ registerDebugConfigurationProvider(debugType: string, provider: DebugConfigurationProvider): Disposable; + /** + * Register a [debug adapter descriptor factory](#DebugAdapterDescriptorFactory) for a specific debug type. + * An extension is only allowed to register a DebugAdapterDescriptorFactory for the debug type(s) defined by the extension. Otherwise an error is thrown. + * Registering more than one DebugAdapterDescriptorFactory for a debug type results in an error. + * + * @param debugType The debug type for which the factory is registered. + * @param factory The [debug adapter descriptor factory](#DebugAdapterDescriptorFactory) to register. + * @return A [disposable](#Disposable) that unregisters this factory when being disposed. + */ + registerDebugAdapterDescriptorFactory(debugType: string, factory: DebugAdapterDescriptorFactory): Disposable; + + /** + * Register a debug adapter tracker factory for the given debug type. + * + * @param debugType The debug type for which the factory is registered or '*' for matching all debug types. + * @param factory The [debug adapter tracker factory](#DebugAdapterTrackerFactory) to register. + * @return A [disposable](#Disposable) that unregisters this factory when being disposed. + */ + registerDebugAdapterTrackerFactory(debugType: string, factory: DebugAdapterTrackerFactory): Disposable; + /** * Start debugging by using either a named launch or named compound configuration, * or by directly passing a [DebugConfiguration](#DebugConfiguration). @@ -739,7 +1030,11 @@ export interface IDebugService { * @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. */ - startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration): Thenable<boolean>; + startDebugging( + folder: WorkspaceFolder | undefined, + nameOrConfiguration: string | DebugConfiguration, + parentSession?: DebugSession | DebugSessionOptions, + ): Thenable<boolean>; /** * Add breakpoints. @@ -761,35 +1056,35 @@ export interface IApplicationEnvironment { * * @readonly */ - appName: string; + readonly appName: string; /** * The extension name. * * @readonly */ - extensionName: string; + readonly extensionName: string; /** * The application root folder from which the editor is running. * * @readonly */ - appRoot: string; + readonly appRoot: string; /** * Represents the preferred user-language, like `de-CH`, `fr`, or `en-US`. * * @readonly */ - language: string; + readonly language: string; /** * A unique identifier for the computer. * * @readonly */ - machineId: string; + readonly machineId: string; /** * A unique identifier for the current session. @@ -797,68 +1092,111 @@ export interface IApplicationEnvironment { * * @readonly */ - sessionId: string; + readonly sessionId: string; /** * Contents of `package.json` as a JSON object. * * @type {any} * @memberof IApplicationEnvironment */ - packageJson: any; -} - -export const IWebPanelMessageListener = Symbol('IWebPanelMessageListener'); -export interface IWebPanelMessageListener extends Disposable { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly packageJson: any; /** - * 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. + * Gets the full path to the user settings file. (may or may not exist). + * + * @type {string} + * @memberof IApplicationShell */ - onMessage(message: string, payload: any): void; -} - -export type WebPanelMessage = { + readonly userSettingsFile: string | undefined; /** - * Message type + * The detected default shell for the extension host, this is overridden by the + * `terminal.integrated.shell` setting for the extension host's platform. + * + * @type {string} + * @memberof IApplicationShell */ - type: string; - + readonly shell: string; /** - * Payload + * An {@link Event} which fires when the default shell changes. */ - payload?: any; -}; - -// Wraps the VS Code webview panel -export const IWebPanel = Symbol('IWebPanel'); -export interface IWebPanel { + readonly onDidChangeShell: Event<string>; /** - * Makes the webpanel show up. - * @return A Promise that can be waited on + * Gets the vscode channel (whether 'insiders' or 'stable'). */ - show(): Promise<void>; - + readonly channel: Channel; /** - * Indicates if this web panel is visible or not. + * Gets the extension channel (whether 'insiders' or 'stable'). + * + * @type {string} + * @memberof IApplicationShell */ - isVisible(): boolean; + readonly extensionChannel: Channel; + /** + * The version of the editor. + */ + readonly vscodeVersion: string; + /** + * The custom uri scheme the editor registers to in the operating system. + */ + readonly uriScheme: string; + /** + * 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. + */ + readonly uiKind: UIKind; + /** + * 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. + * + * *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. + */ + readonly remoteName: string | undefined; +} +export const ILanguageService = Symbol('ILanguageService'); +export interface ILanguageService { /** - * Sends a message to the hosted html page + * Register a completion provider. + * + * Multiple providers can be registered for a language. In that case providers are sorted + * by their [score](#languages.match) and groups of equal score are sequentially asked for + * completion items. The process stops when one or many providers of a group return a + * result. A failing provider (rejected promise or exception) will not fail the whole + * operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A completion provider. + * @param triggerCharacters Trigger completion when the user types one of the characters, like `.` or `:`. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ - postMessage(message: WebPanelMessage); + registerCompletionItemProvider( + selector: DocumentSelector, + provider: CompletionItemProvider, + ...triggerCharacters: string[] + ): Disposable; +} + +/** + * Wraps the `ActiveResourceService` API class. Created for injecting and mocking class methods in testing + */ +export const IActiveResourceService = Symbol('IActiveResourceService'); +export interface IActiveResourceService { + getActiveResource(): Resource; } -// Wraps the VS Code api for creating a web panel -export const IWebPanelProvider = Symbol('IWebPanelProvider'); -export interface IWebPanelProvider { +export const IClipboard = Symbol('IClipboard'); +export interface IClipboard { + /** + * Read the current clipboard contents as text. + */ + readText(): Promise<string>; + /** - * Creates a new webpanel - * @param listener for messages from the panel - * @param title: title of the panel when it shows - * @param: mainScriptPath: full path in the output folder to the script - * @return A IWebPanel that can be used to show html pages. + * Writes text into the clipboard. */ - create(listener: IWebPanelMessageListener, title: string, mainScriptPath: string, embeddedCss?: string): IWebPanel; + writeText(value: string): Promise<void>; } 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/webPanel.ts b/src/client/common/application/webPanel.ts deleted file mode 100644 index 874bfa1eb83b..000000000000 --- a/src/client/common/application/webPanel.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 * as fs from 'fs-extra'; -import * as path from 'path'; -import { Uri, ViewColumn, WebviewPanel, window } from 'vscode'; - -import * as localize from '../../common/utils/localize'; -import { IServiceContainer } from '../../ioc/types'; -import { IDisposableRegistry } from '../types'; -import { IWebPanel, IWebPanelMessageListener, WebPanelMessage } from './types'; - -export class WebPanel implements IWebPanel { - - private listener: IWebPanelMessageListener; - private panel: WebviewPanel | undefined; - private loadPromise: Promise<void>; - private disposableRegistry: IDisposableRegistry; - private rootPath: string; - - constructor( - serviceContainer: IServiceContainer, - listener: IWebPanelMessageListener, - title: string, - mainScriptPath: string, - embeddedCss?: string) { - - this.disposableRegistry = serviceContainer.get<IDisposableRegistry>(IDisposableRegistry); - this.listener = listener; - this.rootPath = path.dirname(mainScriptPath); - this.panel = window.createWebviewPanel( - title.toLowerCase().replace(' ', ''), - title, - {viewColumn: ViewColumn.Two, preserveFocus: true}, - { - enableScripts: true, - retainContextWhenHidden: true, - localResourceRoots: [Uri.file(this.rootPath)] - }); - this.loadPromise = this.load(mainScriptPath, embeddedCss); - } - - public async show() { - await this.loadPromise; - if (this.panel) { - this.panel.reveal(ViewColumn.Two, true); - } - } - - public isVisible() : boolean { - return this.panel ? this.panel.visible : false; - } - - public postMessage(message: WebPanelMessage) { - if (this.panel && this.panel.webview) { - this.panel.webview.postMessage(message); - } - } - - private async load(mainScriptPath: string, embeddedCss?: string) { - if (this.panel) { - if (await fs.pathExists(mainScriptPath)) { - - // 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 = this.generateReactHtml(mainScriptPath, embeddedCss); - - // Reset when the current panel is closed - this.disposableRegistry.push(this.panel.onDidDispose(() => { - this.panel = undefined; - this.listener.dispose(); - })); - - this.disposableRegistry.push(this.panel.webview.onDidReceiveMessage(message => { - // Pass the message onto our listener - this.listener.onMessage(message.type, message.payload); - })); - } else { - // Indicate that we can't load the file path - const badPanelString = localize.DataScience.badWebPanelFormatString(); - this.panel.webview.html = badPanelString.format(mainScriptPath); - } - } - } - - private generateReactHtml(mainScriptPath: string, embeddedCss?: string) { - const uriBasePath = Uri.file(`${path.dirname(mainScriptPath)}/`); - const uriPath = Uri.file(mainScriptPath); - const uriBase = uriBasePath.with({ scheme: 'vscode-resource'}); - const uri = uriPath.with({ scheme: 'vscode-resource' }); - const locDatabase = JSON.stringify(localize.getCollection()); - const style = embeddedCss ? embeddedCss : ''; - - return `<!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> - <base href="${uriBase}"/> - <style type="text/css"> - ${style} - </style> - </head> - <body> - <noscript>You need to enable JavaScript to run this app.</noscript> - <div id="root"></div> - <script type="text/javascript"> - function resolvePath(relativePath) { - if (relativePath && relativePath[0] == '.' && relativePath[1] != '.') { - return "${uriBase}" + relativePath.substring(1); - } - - return "${uriBase}" + relativePath; - } - function getLocStrings() { - return ${locDatabase}; - } - </script> - <script type="text/javascript" src="${uri}"></script></body> - </html>`; - } -} diff --git a/src/client/common/application/webPanelProvider.ts b/src/client/common/application/webPanelProvider.ts deleted file mode 100644 index 8eadee031647..000000000000 --- a/src/client/common/application/webPanelProvider.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 { IServiceContainer } from '../../ioc/types'; -import { IWebPanelMessageListener, IWebPanelProvider } from './types'; -import { WebPanel } from './webPanel'; - -@injectable() -export class WebPanelProvider implements IWebPanelProvider { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - } - - public create(listener: IWebPanelMessageListener, title: string, mainScriptPath: string, embeddedCss?: string) { - return new WebPanel(this.serviceContainer, listener, title, mainScriptPath, embeddedCss); - } -} diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index 96285a73320d..a76a78777bef 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -2,7 +2,22 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; -import { CancellationToken, ConfigurationChangeEvent, Event, FileSystemWatcher, GlobPattern, Uri, workspace, WorkspaceConfiguration, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; +import * as path from 'path'; +import { + CancellationToken, + ConfigurationChangeEvent, + Event, + FileSystemWatcher, + GlobPattern, + TextDocument, + Uri, + workspace, + WorkspaceConfiguration, + WorkspaceFolder, + WorkspaceFoldersChangeEvent, +} from 'vscode'; +import { Resource } from '../types'; +import { getOSType, OSType } from '../utils/platform'; import { IWorkspaceService } from './types'; @injectable() @@ -11,34 +26,100 @@ export class WorkspaceService implements IWorkspaceService { return workspace.onDidChangeConfiguration; } public get rootPath(): string | undefined { - return Array.isArray(workspace.workspaceFolders) ? workspace.workspaceFolders[0].uri.fsPath : undefined; + return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0 + ? workspace.workspaceFolders[0].uri.fsPath + : undefined; } - public get workspaceFolders(): WorkspaceFolder[] | undefined { + public get workspaceFolders(): readonly WorkspaceFolder[] | undefined { return workspace.workspaceFolders; } public get onDidChangeWorkspaceFolders(): Event<WorkspaceFoldersChangeEvent> { 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); + 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: Uri): WorkspaceFolder | undefined { - return workspace.getWorkspaceFolder(uri); + public getWorkspaceFolder(uri: Resource): WorkspaceFolder | undefined { + return uri ? workspace.getWorkspaceFolder(uri) : undefined; } public asRelativePath(pathOrUri: string | Uri, includeWorkspaceFolder?: boolean): string { return workspace.asRelativePath(pathOrUri, includeWorkspaceFolder); } - public createFileSystemWatcher(globPattern: GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): FileSystemWatcher { - return workspace.createFileSystemWatcher(globPattern, ignoreChangeEvents, ignoreChangeEvents, ignoreDeleteEvents); + public createFileSystemWatcher( + globPattern: GlobPattern, + ignoreCreateEvents?: boolean, + ignoreChangeEvents?: boolean, + ignoreDeleteEvents?: boolean, + ): FileSystemWatcher { + return workspace.createFileSystemWatcher( + globPattern, + ignoreCreateEvents, + ignoreChangeEvents, + ignoreDeleteEvents, + ); } - public findFiles(include: GlobPattern, exclude?: GlobPattern, maxResults?: number, token?: CancellationToken): Thenable<Uri[]> { - return workspace.findFiles(include, exclude, maxResults, token); + public findFiles( + include: GlobPattern, + exclude?: GlobPattern, + maxResults?: number, + token?: CancellationToken, + ): Thenable<Uri[]> { + const excludePattern = exclude === undefined ? this.searchExcludes : exclude; + return workspace.findFiles(include, excludePattern, maxResults, token); } - public getWorkspaceFolderIdentifier(resource: Uri): string { + public getWorkspaceFolderIdentifier(resource: Resource, defaultValue: string = ''): string { const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; - return workspaceFolder ? workspaceFolder.uri.fsPath : ''; + return workspaceFolder + ? path.normalize( + 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<void> { + return workspace.onDidGrantWorkspaceTrust; + } + + public openTextDocument(options?: { language?: string; content?: string }): Thenable<TextDocument> { + 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<Uri | undefined> { + 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 fb5ddf2ca66a..000000000000 --- a/src/client/common/asyncDisposableRegistry.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 { IAsyncDisposableRegistry, IDisposable } from './types'; - -// List of disposables that need to run a promise. -@injectable() -export class AsyncDisposableRegistry implements IAsyncDisposableRegistry { - private list : IDisposable[] = []; - - public async dispose(): Promise<void> { - const promises = this.list.map(l => l.dispose()); - await Promise.all(promises); - } - - public push(disposable: IDisposable | undefined) { - if (disposable) { - this.list.push(disposable); - } - } -} diff --git a/src/client/common/cancellation.ts b/src/client/common/cancellation.ts index b325c2ff106a..b24abc7ab493 100644 --- a/src/client/common/cancellation.ts +++ b/src/client/common/cancellation.ts @@ -1,83 +1,128 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { CancellationToken } from 'vscode-jsonrpc'; - -import { createDeferred } from './utils/async'; -import * as localize from './utils/localize'; - -/** - * Error type thrown when canceling. - */ -export class CancellationError extends Error { - - constructor() { - super(localize.Common.canceled()); - } -} - -export namespace Cancellation { - - /** - * Races a promise and cancellation. Promise can take a cancellation token too in order to listen to cancellation. - * @param work function returning a promise to race - * @param token token used for cancellation - */ - export function race<T>(work : (token?: CancellationToken) => Promise<T>, token?: CancellationToken) : Promise<T> { - if (token) { - // Use a deferred promise. Resolves when the work finishes - const deferred = createDeferred<T>(); - - // Cancel the deferred promise when the cancellation happens - token.onCancellationRequested(() => { - if (!deferred.completed) { - deferred.reject(new CancellationError()); - } - }); - - // Might already be canceled - if (token.isCancellationRequested) { - // Just start out as rejected - deferred.reject(new CancellationError()); - } else { - // Not canceled yet. When the work finishes - // either resolve our promise or cancel. - work(token) - .then((v) => { - if (!deferred.completed) { - deferred.resolve(v); - } - }) - .catch((e) => { - if (!deferred.completed) { - deferred.reject(e); - } - }); - } - - return deferred.promise; - } else { - // No actual token, just do the original work. - return work(); - } - } - - /** - * isCanceled returns a boolean indicating if the cancel token has been canceled. - * @param cancelToken - */ - export function isCanceled(cancelToken?: CancellationToken) : boolean { - return cancelToken ? cancelToken.isCancellationRequested : false; - } - - /** - * throws a CancellationError if the token is canceled. - * @param cancelToken - */ - export function throwIfCanceled(cancelToken?: CancellationToken) : void { - if (isCanceled(cancelToken)) { - throw new CancellationError(); - } - } - -} +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { CancellationToken, CancellationTokenSource, CancellationError as VSCCancellationError } from 'vscode'; +import { createDeferred } from './utils/async'; +import * as localize from './utils/localize'; + +/** + * Error type thrown when canceling. + */ +export class CancellationError extends Error { + constructor() { + super(localize.Common.canceled); + } + + 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 function createPromiseFromCancellation<T>(options: { + defaultValue: T; + token?: CancellationToken; + cancelAction: 'reject' | 'resolve'; +}): Promise<T> { + return new Promise<T>((resolve, reject) => { + // Never resolve. + if (!options.token) { + return; + } + const complete = () => { + const optionsToken = options.token!; // NOSONAR + if (optionsToken.isCancellationRequested) { + if (options.cancelAction === 'resolve') { + return resolve(options.defaultValue); + } + if (options.cancelAction === 'reject') { + return reject(new CancellationError()); + } + } + }; + + options.token.onCancellationRequested(complete); + }); +} + +/** + * Create a single unified cancellation token that wraps multiple cancellation tokens. + */ +export function wrapCancellationTokens(...tokens: (CancellationToken | undefined)[]): CancellationToken { + const wrappedCancellantionToken = new CancellationTokenSource(); + for (const token of tokens) { + if (!token) { + continue; + } + if (token.isCancellationRequested) { + return token; + } + token.onCancellationRequested(() => wrappedCancellantionToken.cancel()); + } + + return wrappedCancellantionToken.token; +} + +export namespace Cancellation { + /** + * Races a promise and cancellation. Promise can take a cancellation token too in order to listen to cancellation. + * @param work function returning a promise to race + * @param token token used for cancellation + */ + export function race<T>(work: (token?: CancellationToken) => Promise<T>, token?: CancellationToken): Promise<T> { + if (token) { + // Use a deferred promise. Resolves when the work finishes + const deferred = createDeferred<T>(); + + // Cancel the deferred promise when the cancellation happens + token.onCancellationRequested(() => { + if (!deferred.completed) { + deferred.reject(new CancellationError()); + } + }); + + // Might already be canceled + if (token.isCancellationRequested) { + // Just start out as rejected + deferred.reject(new CancellationError()); + } else { + // Not canceled yet. When the work finishes + // either resolve our promise or cancel. + work(token) + .then((v) => { + if (!deferred.completed) { + deferred.resolve(v); + } + }) + .catch((e) => { + if (!deferred.completed) { + deferred.reject(e); + } + }); + } + + return deferred.promise; + } else { + // No actual token, just do the original work. + return work(); + } + } + + /** + * isCanceled returns a boolean indicating if the cancel token has been canceled. + */ + export function isCanceled(cancelToken?: CancellationToken): boolean { + return cancelToken ? cancelToken.isCancellationRequested : false; + } + + /** + * throws a CancellationError if the token is canceled. + */ + export function throwIfCanceled(cancelToken?: CancellationToken): void { + if (isCanceled(cancelToken)) { + throw new CancellationError(); + } + } +} diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index b10c7ad0c686..91c06d9331fd 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -1,451 +1,563 @@ -'use strict'; - -import * as child_process from 'child_process'; -import { EventEmitter } from 'events'; -import * as path from 'path'; -import { ConfigurationChangeEvent, ConfigurationTarget, DiagnosticSeverity, Disposable, Uri, WorkspaceConfiguration } from 'vscode'; -import '../common/extensions'; -import { IInterpreterAutoSeletionProxyService } from '../interpreter/autoSelection/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { COMPLETION_ADD_BRACKETS, FORMAT_ON_TYPE } from '../telemetry/constants'; -import { IWorkspaceService } from './application/types'; -import { WorkspaceService } from './application/workspace'; -import { isTestExecution } from './constants'; -import { IS_WINDOWS } from './platform/constants'; -import { - IAnalysisSettings, - IAutoCompleteSettings, - IDataScienceSettings, - IFormattingSettings, - ILintingSettings, - IPythonSettings, - ISortImportSettings, - ITerminalSettings, - IUnitTestSettings, - IWorkspaceSymbolSettings -} from './types'; -import { SystemVariables } from './variables/systemVariables'; - -// tslint:disable-next-line:no-require-imports no-var-requires -const untildify = require('untildify'); - -// tslint:disable-next-line:completed-docs -export class PythonSettings extends EventEmitter implements IPythonSettings { - private static pythonSettings: Map<string, PythonSettings> = new Map<string, PythonSettings>(); - public downloadLanguageServer = true; - public jediEnabled = true; - public jediPath = ''; - public jediMemoryLimit = 1024; - public envFile = ''; - public venvPath = ''; - public venvFolders: string[] = []; - public condaPath = ''; - public devOptions: string[] = []; - public linting!: ILintingSettings; - public formatting!: IFormattingSettings; - public autoComplete!: IAutoCompleteSettings; - public unitTest!: IUnitTestSettings; - 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; - - private workspaceRoot: Uri; - private disposables: Disposable[] = []; - // tslint:disable-next-line:variable-name - private _pythonPath = ''; - private readonly workspace: IWorkspaceService; - - constructor(workspaceFolder: Uri | undefined, private readonly InterpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, - workspace?: IWorkspaceService) { - super(); - this.workspace = workspace || new WorkspaceService(); - this.workspaceRoot = workspaceFolder ? workspaceFolder : Uri.file(__dirname); - this.initialize(); - } - // tslint:disable-next-line:function-name - public static getInstance(resource: Uri | undefined, interpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, - workspace?: IWorkspaceService): PythonSettings { - workspace = workspace || new WorkspaceService(); - const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource, workspace).uri; - const workspaceFolderKey = workspaceFolderUri ? workspaceFolderUri.fsPath : ''; - - if (!PythonSettings.pythonSettings.has(workspaceFolderKey)) { - const settings = new PythonSettings(workspaceFolderUri, interpreterAutoSelectionService, workspace); - PythonSettings.pythonSettings.set(workspaceFolderKey, settings); - // Pass null to avoid VSC from complaining about not passing in a value. - // tslint:disable-next-line:no-any - const config = workspace.getConfiguration('editor', resource ? resource : null as any); - const formatOnType = config ? config.get('formatOnType', false) : false; - sendTelemetryEvent(COMPLETION_ADD_BRACKETS, undefined, { enabled: settings.autoComplete ? settings.autoComplete.addBrackets : false }); - sendTelemetryEvent(FORMAT_ON_TYPE, undefined, { enabled: formatOnType }); - } - // tslint:disable-next-line:no-non-null-assertion - return PythonSettings.pythonSettings.get(workspaceFolderKey)!; - } - - // tslint:disable-next-line:type-literal-delimiter - public static getSettingsUriAndTarget(resource: Uri | undefined, workspace?: IWorkspaceService): { uri: Uri | undefined, target: ConfigurationTarget } { - workspace = workspace || new WorkspaceService(); - const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; - let workspaceFolderUri: Uri | undefined = workspaceFolder ? workspaceFolder.uri : undefined; - - if (!workspaceFolderUri && Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { - workspaceFolderUri = workspace.workspaceFolders[0].uri; - } - - const target = workspaceFolderUri ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Global; - return { uri: workspaceFolderUri, target }; - } - - // tslint:disable-next-line:function-name - public static dispose() { - if (!isTestExecution()) { - throw new Error('Dispose can only be called from unit tests'); - } - // tslint:disable-next-line:no-void-expression - PythonSettings.pythonSettings.forEach(item => item && item.dispose()); - PythonSettings.pythonSettings.clear(); - } - public dispose() { - // tslint:disable-next-line:no-unsafe-any - this.disposables.forEach(disposable => disposable && disposable.dispose()); - this.disposables = []; - } - // tslint:disable-next-line:cyclomatic-complexity max-func-body-length - protected update(pythonSettings: WorkspaceConfiguration) { - const workspaceRoot = this.workspaceRoot.fsPath; - const systemVariables: SystemVariables = new SystemVariables(this.workspaceRoot ? this.workspaceRoot.fsPath : undefined); - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - this.pythonPath = systemVariables.resolveAny(pythonSettings.get<string>('pythonPath'))!; - // If user has defined a custom value, use it else try to get the best interpreter ourselves. - if (this.pythonPath.length === 0 || this.pythonPath === 'python') { - const autoSelectedPythonInterpreter = this.InterpreterAutoSelectionService.getAutoSelectedInterpreter(this.workspaceRoot); - if (autoSelectedPythonInterpreter) { - this.InterpreterAutoSelectionService.setWorkspaceInterpreter(this.workspaceRoot, autoSelectedPythonInterpreter).ignoreErrors(); - } - this.pythonPath = autoSelectedPythonInterpreter ? autoSelectedPythonInterpreter.path : this.pythonPath; - } - this.pythonPath = getAbsolutePath(this.pythonPath, workspaceRoot); - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - this.venvPath = systemVariables.resolveAny(pythonSettings.get<string>('venvPath'))!; - this.venvFolders = systemVariables.resolveAny(pythonSettings.get<string[]>('venvFolders'))!; - const condaPath = systemVariables.resolveAny(pythonSettings.get<string>('condaPath'))!; - this.condaPath = condaPath && condaPath.length > 0 ? getAbsolutePath(condaPath, workspaceRoot) : condaPath; - - this.downloadLanguageServer = systemVariables.resolveAny(pythonSettings.get<boolean>('downloadLanguageServer', true))!; - this.jediEnabled = systemVariables.resolveAny(pythonSettings.get<boolean>('jediEnabled', true))!; - this.autoUpdateLanguageServer = systemVariables.resolveAny(pythonSettings.get<boolean>('autoUpdateLanguageServer', true))!; - if (this.jediEnabled) { - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - this.jediPath = systemVariables.resolveAny(pythonSettings.get<string>('jediPath'))!; - if (typeof this.jediPath === 'string' && this.jediPath.length > 0) { - this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), workspaceRoot); - } else { - this.jediPath = ''; - } - this.jediMemoryLimit = pythonSettings.get<number>('jediMemoryLimit')!; - } - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - this.envFile = systemVariables.resolveAny(pythonSettings.get<string>('envFile'))!; - // tslint:disable-next-line:no-any - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion no-any - this.devOptions = systemVariables.resolveAny(pythonSettings.get<any[]>('devOptions'))!; - this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const lintingSettings = systemVariables.resolveAny(pythonSettings.get<ILintingSettings>('linting'))!; - if (this.linting) { - Object.assign<ILintingSettings, ILintingSettings>(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<IAnalysisSettings>('analysis'))!; - if (this.analysis) { - Object.assign<IAnalysisSettings, IAnalysisSettings>(this.analysis, analysisSettings); - } else { - this.analysis = analysisSettings; - } - - this.disableInstallationChecks = pythonSettings.get<boolean>('disableInstallationCheck') === true; - this.globalModuleInstallation = pythonSettings.get<boolean>('globalModuleInstallation') === true; - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const sortImportSettings = systemVariables.resolveAny(pythonSettings.get<ISortImportSettings>('sortImports'))!; - if (this.sortImports) { - Object.assign<ISortImportSettings, ISortImportSettings>(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', - pep8Args: [], pep8Enabled: false, pep8Path: 'pep8', - 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 - }, - pep8CategorySeverity: { - 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.pep8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.pep8Path), 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<IFormattingSettings>('formatting'))!; - if (this.formatting) { - Object.assign<IFormattingSettings, IFormattingSettings>(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<IAutoCompleteSettings>('autoComplete'))!; - if (this.autoComplete) { - Object.assign<IAutoCompleteSettings, IAutoCompleteSettings>(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<IWorkspaceSymbolSettings>('workspaceSymbols'))!; - if (this.workspaceSymbols) { - Object.assign<IWorkspaceSymbolSettings, IWorkspaceSymbolSettings>(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: 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 unitTestSettings = systemVariables.resolveAny(pythonSettings.get<IUnitTestSettings>('unitTest'))!; - if (this.unitTest) { - Object.assign<IUnitTestSettings, IUnitTestSettings>(this.unitTest, unitTestSettings); - } else { - this.unitTest = unitTestSettings; - if (isTestExecution() && !this.unitTest) { - // tslint:disable-next-line:prefer-type-cast - // tslint:disable-next-line:no-object-literal-type-assertion - this.unitTest = { - nosetestArgs: [], pyTestArgs: [], unittestArgs: [], - promptToConfigure: true, debugPort: 3000, - nosetestsEnabled: false, pyTestEnabled: false, unittestEnabled: false, - nosetestPath: 'nosetests', pyTestPath: 'pytest', autoTestDiscoverOnSaveEnabled: true - } as IUnitTestSettings; - } - } - - // Support for travis. - this.unitTest = this.unitTest ? this.unitTest : { - promptToConfigure: true, - debugPort: 3000, - nosetestArgs: [], nosetestPath: 'nosetest', nosetestsEnabled: false, - pyTestArgs: [], pyTestEnabled: false, pyTestPath: 'pytest', - unittestArgs: [], unittestEnabled: false, autoTestDiscoverOnSaveEnabled: true - }; - this.unitTest.pyTestPath = getAbsolutePath(systemVariables.resolveAny(this.unitTest.pyTestPath), workspaceRoot); - this.unitTest.nosetestPath = getAbsolutePath(systemVariables.resolveAny(this.unitTest.nosetestPath), workspaceRoot); - if (this.unitTest.cwd) { - this.unitTest.cwd = getAbsolutePath(systemVariables.resolveAny(this.unitTest.cwd), workspaceRoot); - } - - // Resolve any variables found in the test arguments. - this.unitTest.nosetestArgs = this.unitTest.nosetestArgs.map(arg => systemVariables.resolveAny(arg)); - this.unitTest.pyTestArgs = this.unitTest.pyTestArgs.map(arg => systemVariables.resolveAny(arg)); - this.unitTest.unittestArgs = this.unitTest.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<ITerminalSettings>('terminal'))!; - if (this.terminal) { - Object.assign<ITerminalSettings, ITerminalSettings>(this.terminal, terminalSettings); - } else { - this.terminal = terminalSettings; - if (isTestExecution() && !this.terminal) { - // tslint:disable-next-line:prefer-type-cast - // tslint:disable-next-line:no-object-literal-type-assertion - this.terminal = {} as ITerminalSettings; - } - } - // Support for travis. - this.terminal = this.terminal ? this.terminal : { - executeInFileDir: true, - launchArgs: [], - activateEnvironment: true - }; - - const dataScienceSettings = systemVariables.resolveAny(pythonSettings.get<IDataScienceSettings>('dataScience'))!; - if (this.datascience) { - Object.assign<IDataScienceSettings, IDataScienceSettings>(this.datascience, dataScienceSettings); - } else { - this.datascience = dataScienceSettings; - } - } - - public get pythonPath(): string { - return this._pythonPath; - } - public set pythonPath(value: string) { - if (this._pythonPath === value) { - return; - } - // Add support for specifying just the directory where the python executable will be located. - // E.g. virtual directory name. - try { - this._pythonPath = this.getPythonExecutable(value); - } catch (ex) { - this._pythonPath = value; - } - } - protected getPythonExecutable(pythonPath: string) { - return getPythonExecutable(pythonPath); - } - 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. - setTimeout(() => this.emit('change'), 1); - }; - this.disposables.push(this.InterpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(onDidChange.bind(this))); - this.disposables.push(this.workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => { - if (event.affectsConfiguration('python')) { - onDidChange(); - } - })); - - const initialConfig = this.workspace.getConfiguration('python', this.workspaceRoot); - if (initialConfig) { - this.update(initialConfig); - } - } -} - -function getAbsolutePath(pathToCheck: string, rootDir: string): string { - // tslint:disable-next-line:prefer-type-cast no-unsafe-any - pathToCheck = untildify(pathToCheck) as string; - if (isTestExecution() && !pathToCheck) { return rootDir; } - if (pathToCheck.indexOf(path.sep) === -1) { - return pathToCheck; - } - return path.isAbsolute(pathToCheck) ? pathToCheck : path.resolve(rootDir, pathToCheck); -} - -function getPythonExecutable(pythonPath: string): string { - // tslint:disable-next-line:prefer-type-cast no-unsafe-any - pythonPath = untildify(pythonPath) as string; - - // If only 'python'. - if (pythonPath === 'python' || - pythonPath.indexOf(path.sep) === -1 || - path.basename(pythonPath) === path.dirname(pythonPath)) { - return pythonPath; - } - - if (isValidPythonPath(pythonPath)) { - return pythonPath; - } - // Keep python right on top, for backwards compatibility. - // tslint:disable-next-line:variable-name - const KnownPythonExecutables = ['python', 'python4', 'python3.6', 'python3.5', 'python3', 'python2.7', 'python2']; - - for (let executableName of KnownPythonExecutables) { - // Suffix with 'python' for linux and 'osx', and 'python.exe' for 'windows'. - if (IS_WINDOWS) { - executableName = `${executableName}.exe`; - if (isValidPythonPath(path.join(pythonPath, executableName))) { - return path.join(pythonPath, executableName); - } - if (isValidPythonPath(path.join(pythonPath, 'scripts', executableName))) { - return path.join(pythonPath, 'scripts', executableName); - } - } else { - if (isValidPythonPath(path.join(pythonPath, executableName))) { - return path.join(pythonPath, executableName); - } - if (isValidPythonPath(path.join(pythonPath, 'bin', executableName))) { - return path.join(pythonPath, 'bin', executableName); - } - } - } - - return pythonPath; -} - -function isValidPythonPath(pythonPath: string): boolean { - try { - const output = child_process.execFileSync(pythonPath, ['-c', 'print(1234)'], { encoding: 'utf8' }); - return output.startsWith('1234'); - } catch (ex) { - return false; - } -} +'use strict'; + +// eslint-disable-next-line camelcase +import * as path from 'path'; +import * as fs from 'fs'; +import { + ConfigurationChangeEvent, + ConfigurationTarget, + Disposable, + Event, + EventEmitter, + Uri, + WorkspaceConfiguration, +} from 'vscode'; +import { LanguageServerType } from '../activation/types'; +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, PYREFLY_EXTENSION_ID } from './constants'; +import { + IAutoCompleteSettings, + IDefaultLanguageServer, + IExperiments, + IExtensions, + IInterpreterPathService, + IInterpreterSettings, + IPythonSettings, + IREPLSettings, + ITerminalSettings, + Resource, +} from './types'; +import { debounceSync } from './utils/decorators'; +import { SystemVariables } from './variables/systemVariables'; +import { getOSType, OSType, isWindows } from './utils/platform'; +import { untildify } from './helpers'; + +export class PythonSettings implements IPythonSettings { + private get onDidChange(): Event<ConfigurationChangeEvent | undefined> { + return this.changed.event; + } + + // eslint-disable-next-line class-methods-use-this + public static onConfigChange(): Event<ConfigurationChangeEvent | undefined> { + return PythonSettings.configChanged.event; + } + + public get pythonPath(): string { + return this._pythonPath; + } + + public set pythonPath(value: string) { + if (this._pythonPath === value) { + return; + } + // Add support for specifying just the directory where the python executable will be located. + // E.g. virtual directory name. + try { + this._pythonPath = this.getPythonExecutable(value); + } catch (ex) { + this._pythonPath = value; + } + } + + public get defaultInterpreterPath(): string { + return this._defaultInterpreterPath; + } + + public set defaultInterpreterPath(value: string) { + if (this._defaultInterpreterPath === value) { + return; + } + // Add support for specifying just the directory where the python executable will be located. + // E.g. virtual directory name. + try { + this._defaultInterpreterPath = this.getPythonExecutable(value); + } catch (ex) { + this._defaultInterpreterPath = value; + } + } + + private static pythonSettings: Map<string, PythonSettings> = new Map<string, PythonSettings>(); + + public envFile = ''; + + public venvPath = ''; + + public interpreter!: IInterpreterSettings; + + public venvFolders: string[] = []; + + public activeStateToolPath = ''; + + public condaPath = ''; + + public pipenvPath = ''; + + public poetryPath = ''; + + public pixiToolPath = ''; + + public devOptions: string[] = []; + + public autoComplete!: IAutoCompleteSettings; + + public testing!: ITestingSettings; + + public terminal!: ITerminalSettings; + + public globalModuleInstallation = false; + + public REPL!: IREPLSettings; + + public experiments!: IExperiments; + + public languageServer: LanguageServerType = LanguageServerType.Node; + + public languageServerIsDefault = true; + + protected readonly changed = new EventEmitter<ConfigurationChangeEvent | undefined>(); + + private static readonly configChanged = new EventEmitter<ConfigurationChangeEvent | undefined>(); + + private workspaceRoot: Resource; + + private disposables: Disposable[] = []; + + private _pythonPath = 'python'; + + private _defaultInterpreterPath = ''; + + private readonly workspace: IWorkspaceService; + + constructor( + workspaceFolder: Resource, + 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(); + } + + public static getInstance( + resource: Uri | undefined, + interpreterAutoSelectionService: IInterpreterAutoSelectionProxyService, + workspace: IWorkspaceService, + interpreterPathService: IInterpreterPathService, + defaultLS: IDefaultLanguageServer | undefined, + extensions: IExtensions, + ): PythonSettings { + workspace = workspace || new WorkspaceService(); + const workspaceFolderUri = PythonSettings.getSettingsUriAndTarget(resource, workspace).uri; + const workspaceFolderKey = workspaceFolderUri ? workspaceFolderUri.fsPath : ''; + + if (!PythonSettings.pythonSettings.has(workspaceFolderKey)) { + const settings = new PythonSettings( + workspaceFolderUri, + interpreterAutoSelectionService, + workspace, + interpreterPathService, + 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. + + // 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.FORMAT_ON_TYPE, undefined, { enabled: formatOnType }); + } + + return PythonSettings.pythonSettings.get(workspaceFolderKey)!; + } + + @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, + ): { uri: Uri | undefined; target: ConfigurationTarget } { + workspace = workspace || new WorkspaceService(); + const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; + let workspaceFolderUri: Uri | undefined = workspaceFolder ? workspaceFolder.uri : undefined; + + if (!workspaceFolderUri && Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + workspaceFolderUri = workspace.workspaceFolders[0].uri; + } + + const target = workspaceFolderUri ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Global; + return { uri: workspaceFolderUri, target }; + } + + public static dispose(): void { + if (!isTestExecution()) { + throw new Error('Dispose can only be called from unit tests'); + } + + PythonSettings.pythonSettings.forEach((item) => item && item.dispose()); + PythonSettings.pythonSettings.clear(); + } + + 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 = []; + } + + protected update(pythonSettings: WorkspaceConfiguration): void { + const workspaceRoot = this.workspaceRoot?.fsPath; + const systemVariables: SystemVariables = new SystemVariables(undefined, workspaceRoot, this.workspace); + + this.pythonPath = this.getPythonPath(systemVariables, workspaceRoot); + + const defaultInterpreterPath = systemVariables.resolveAny(pythonSettings.get<string>('defaultInterpreterPath')); + 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); + + this.venvPath = systemVariables.resolveAny(pythonSettings.get<string>('venvPath'))!; + this.venvFolders = systemVariables.resolveAny(pythonSettings.get<string[]>('venvFolders'))!; + const activeStateToolPath = systemVariables.resolveAny(pythonSettings.get<string>('activeStateToolPath'))!; + this.activeStateToolPath = + activeStateToolPath && activeStateToolPath.length > 0 + ? getAbsolutePath(activeStateToolPath, workspaceRoot) + : activeStateToolPath; + const condaPath = systemVariables.resolveAny(pythonSettings.get<string>('condaPath'))!; + this.condaPath = condaPath && condaPath.length > 0 ? getAbsolutePath(condaPath, workspaceRoot) : condaPath; + const pipenvPath = systemVariables.resolveAny(pythonSettings.get<string>('pipenvPath'))!; + this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath; + const poetryPath = systemVariables.resolveAny(pythonSettings.get<string>('poetryPath'))!; + this.poetryPath = poetryPath && poetryPath.length > 0 ? getAbsolutePath(poetryPath, workspaceRoot) : poetryPath; + const pixiToolPath = systemVariables.resolveAny(pythonSettings.get<string>('pixiToolPath'))!; + this.pixiToolPath = + pixiToolPath && pixiToolPath.length > 0 ? getAbsolutePath(pixiToolPath, workspaceRoot) : pixiToolPath; + + this.interpreter = pythonSettings.get<IInterpreterSettings>('interpreter') ?? { + infoVisibility: 'onPythonRelated', + }; + // Get as a string and verify; don't just accept. + let userLS = pythonSettings.get<string>('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<boolean>('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; + } + + const autoCompleteSettings = systemVariables.resolveAny( + pythonSettings.get<IAutoCompleteSettings>('autoComplete'), + )!; + if (this.autoComplete) { + Object.assign<IAutoCompleteSettings, IAutoCompleteSettings>(this.autoComplete, autoCompleteSettings); + } else { + this.autoComplete = autoCompleteSettings; + } + + const envFileSetting = pythonSettings.get<string>('envFile'); + this.envFile = systemVariables.resolveAny(envFileSetting)!; + sendSettingTelemetry(this.workspace, envFileSetting); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.devOptions = systemVariables.resolveAny(pythonSettings.get<any[]>('devOptions'))!; + this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; + + this.globalModuleInstallation = pythonSettings.get<boolean>('globalModuleInstallation') === true; + + const testSettings = systemVariables.resolveAny(pythonSettings.get<ITestingSettings>('testing'))!; + if (this.testing) { + Object.assign<ITestingSettings, ITestingSettings>(this.testing, testSettings); + } else { + this.testing = testSettings; + if (isTestExecution() && !this.testing) { + this.testing = { + pytestArgs: [], + unittestArgs: [], + promptToConfigure: true, + debugPort: 3000, + pytestEnabled: false, + unittestEnabled: false, + pytestPath: 'pytest', + autoTestDiscoverOnSaveEnabled: true, + autoTestDiscoverOnSavePattern: '**/*.py', + } as ITestingSettings; + } + } + + // Support for travis. + this.testing = this.testing + ? this.testing + : { + promptToConfigure: true, + debugPort: 3000, + pytestArgs: [], + pytestEnabled: false, + pytestPath: 'pytest', + unittestArgs: [], + unittestEnabled: false, + autoTestDiscoverOnSaveEnabled: true, + autoTestDiscoverOnSavePattern: '**/*.py', + }; + this.testing.pytestPath = getAbsolutePath(systemVariables.resolveAny(this.testing.pytestPath), 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.pytestArgs = this.testing.pytestArgs.map((arg) => systemVariables.resolveAny(arg)); + this.testing.unittestArgs = this.testing.unittestArgs.map((arg) => systemVariables.resolveAny(arg)); + + const terminalSettings = systemVariables.resolveAny(pythonSettings.get<ITerminalSettings>('terminal'))!; + if (this.terminal) { + Object.assign<ITerminalSettings, ITerminalSettings>(this.terminal, terminalSettings); + } else { + this.terminal = terminalSettings; + if (isTestExecution() && !this.terminal) { + this.terminal = {} as ITerminalSettings; + } + } + // Support for travis. + this.terminal = this.terminal + ? this.terminal + : { + executeInFileDir: true, + focusAfterLaunch: false, + launchArgs: [], + activateEnvironment: true, + activateEnvInCurrentTerminal: false, + shellIntegration: { + enabled: false, + }, + }; + + this.REPL = pythonSettings.get<IREPLSettings>('REPL')!; + const experiments = pythonSettings.get<IExperiments>('experiments')!; + if (this.experiments) { + Object.assign<IExperiments, IExperiments>(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: [], + }; + } + + // eslint-disable-next-line class-methods-use-this + protected getPythonExecutable(pythonPath: string): string { + return getPythonExecutable(pythonPath); + } + + 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); + if (activatedWkspcFoldersRemoved.length > 0) { + for (const folder of activatedWkspcFoldersRemoved) { + PythonSettings.pythonSettings.delete(folder); + } + } + } + + 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(() => { + this.onDidChanged(); + }), + ); + this.disposables.push( + this.workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => { + if (event.affectsConfiguration('python')) { + this.onDidChanged(event); + } + }), + ); + if (this.interpreterPathService) { + this.disposables.push( + this.interpreterPathService.onDidChange(() => { + this.onDidChanged(); + }), + ); + } + + const initialConfig = this.workspace.getConfiguration('python', this.workspaceRoot); + if (initialConfig) { + this.update(initialConfig); + } + } + + @debounceSync(1) + protected debounceChangeNotification(event?: ConfigurationChangeEvent): void { + this.changed.fire(event); + } + + 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, + ); + if (autoSelectedPythonInterpreter) { + this.pythonPath = autoSelectedPythonInterpreter.path; + if (this.workspaceRoot) { + this.interpreterAutoSelectionService + .setWorkspaceInterpreter(this.workspaceRoot, autoSelectedPythonInterpreter) + .ignoreErrors(); + } + } + } + return getAbsolutePath(this.pythonPath, workspaceRoot); + } +} + +function getAbsolutePath(pathToCheck: string, rootDir: string | undefined): string { + if (!rootDir) { + rootDir = __dirname; + } + + pathToCheck = untildify(pathToCheck) as string; + if (isTestExecution() && !pathToCheck) { + return rootDir; + } + if (pathToCheck.indexOf(path.sep) === -1) { + return pathToCheck; + } + return path.isAbsolute(pathToCheck) ? pathToCheck : path.resolve(rootDir, pathToCheck); +} + +function getPythonExecutable(pythonPath: string): string { + pythonPath = untildify(pythonPath) as string; + + // If only 'python'. + if ( + pythonPath === 'python' || + pythonPath.indexOf(path.sep) === -1 || + path.basename(pythonPath) === path.dirname(pythonPath) + ) { + return pythonPath; + } + + if (isValidPythonPath(pythonPath)) { + return pythonPath; + } + // Keep python right on top, for backwards compatibility. + + 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 (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); + } + } else { + if (isValidPythonPath(path.join(pythonPath, executableName))) { + return path.join(pythonPath, executableName); + } + if (isValidPythonPath(path.join(pythonPath, 'bin', executableName))) { + return path.join(pythonPath, 'bin', executableName); + } + } + } + + return pythonPath; +} + +function isValidPythonPath(pythonPath: string): boolean { + 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 505039a181ed..443990b2e5da 100644 --- a/src/client/common/configuration/service.ts +++ b/src/client/common/configuration/service.ts @@ -2,58 +2,103 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { ConfigurationTarget, Uri, workspace, WorkspaceConfiguration } from 'vscode'; -import { IInterpreterAutoSeletionProxyService } 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 { IConfigurationService, IPythonSettings } from '../types'; +import { isUnitTestExecution } from '../constants'; +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>(IWorkspaceService); } + + // eslint-disable-next-line class-methods-use-this + public get onDidChange(): Event<ConfigurationChangeEvent | undefined> { + return PythonSettings.onConfigChange(); + } + public getSettings(resource?: Uri): IPythonSettings { - const InterpreterAutoSelectionService = this.serviceContainer.get<IInterpreterAutoSeletionProxyService>(IInterpreterAutoSeletionProxyService); - return PythonSettings.getInstance(resource, InterpreterAutoSelectionService, this.workspaceService); + const InterpreterAutoSelectionService = this.serviceContainer.get<IInterpreterAutoSelectionService>( + IInterpreterAutoSelectionService, + ); + const interpreterPathService = this.serviceContainer.get<IInterpreterPathService>(IInterpreterPathService); + const defaultLS = this.serviceContainer.tryGet<IDefaultLanguageServer>(IDefaultLanguageServer); + const extensions = this.serviceContainer.get<IExtensions>(IExtensions); + return PythonSettings.getInstance( + resource, + InterpreterAutoSelectionService, + this.workspaceService, + interpreterPathService, + defaultLS, + extensions, + ); } - public async updateSectionSetting(section: string, setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise<void> { + public async updateSectionSetting( + section: string, + setting: string, + value?: unknown, + resource?: Uri, + configTarget?: ConfigurationTarget, + ): Promise<void> { 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 || settingsInfo.target; - const configSection = workspace.getConfiguration(section, settingsInfo.uri); + const configSection = this.workspaceService.getConfiguration(section, settingsInfo.uri); const currentValue = configSection.inspect(setting); - if (currentValue !== undefined && - ((settingsInfo.target === ConfigurationTarget.Global && currentValue.globalValue === value) || - (settingsInfo.target === ConfigurationTarget.Workspace && currentValue.workspaceValue === value) || - (settingsInfo.target === ConfigurationTarget.WorkspaceFolder && currentValue.workspaceFolderValue === value))) { + if ( + currentValue !== undefined && + ((configTarget === ConfigurationTarget.Global && currentValue.globalValue === value) || + (configTarget === ConfigurationTarget.Workspace && currentValue.workspaceValue === value) || + (configTarget === ConfigurationTarget.WorkspaceFolder && currentValue.workspaceFolderValue === value)) + ) { return; } - - await configSection.update(setting, value, settingsInfo.target); - await this.verifySetting(configSection, settingsInfo.target, setting, value); + await configSection.update(setting, value, configTarget); + await this.verifySetting(configSection, configTarget, setting, value); } - public async updateSetting(setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise<void> { + public async updateSetting( + setting: string, + value?: unknown, + resource?: Uri, + configTarget?: ConfigurationTarget, + ): Promise<void> { 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'; } - private async verifySetting(configSection: WorkspaceConfiguration, target: ConfigurationTarget, settingName: string, value?: {}): Promise<void> { - if (this.isTestExecution()) { + private async verifySetting( + configSection: WorkspaceConfiguration, + target: ConfigurationTarget, + settingName: string, + value?: unknown, + ): Promise<void> { + if (this.isTestExecution() && !isUnitTestExecution()) { let retries = 0; do { const setting = configSection.inspect(settingName); @@ -62,15 +107,20 @@ 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; } } // Wait for settings to get refreshed. - await new Promise((resolve, reject) => setTimeout(resolve, 250)); + await new Promise((resolve) => setTimeout(resolve, 250)); retries += 1; } while (retries < 20); } diff --git a/src/client/common/constants.ts b/src/client/common/constants.ts index 9874d179eed5..15fd037a3d9f 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -1,75 +1,107 @@ +/* eslint-disable camelcase */ +/* eslint-disable @typescript-eslint/no-namespace */ export const PYTHON_LANGUAGE = 'python'; +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: 'untitled', language: PYTHON_LANGUAGE }, + { scheme: 'vscode-notebook', 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 PVSC_EXTENSION_ID = 'ms-python.python'; +export const PYLANCE_EXTENSION_ID = 'ms-python.vscode-pylance'; +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_Selection_In_Terminal = 'python.execSelectionInTerminal'; + export const Exec_In_Terminal_Icon = 'python.execInTerminal-icon'; + 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 Tests_Discover = 'python.discoverTests'; - export const Tests_Run_Failed = 'python.runFailedTests'; - export const Sort_Imports = 'python.sortImports'; - export const Tests_Run = 'python.runtests'; - export const Tests_Debug = 'python.debugtests'; - export const Tests_Ask_To_Stop_Test = 'python.askToStopUnitTests'; - export const Tests_Ask_To_Stop_Discovery = 'python.askToStopUnitTestDiscovery'; - export const Tests_Stop = 'python.stopUnitTests'; - 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 Refaactor_Extract_Method = 'python.refactorExtractMethod'; - export const Update_SparkLibrary = 'python.updateSparkLibrary'; - export const Build_Workspace_Symbols = 'python.buildWorkspaceSymbols'; + 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 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 Start_Native_REPL = 'python.startNativeREPL'; + export const Tests_Configure = 'python.configureTests'; + export const Tests_CopilotSetup = 'python.copilotSetupTests'; + export const TriggerEnvironmentSelection = 'python.triggerEnvSelection'; + export const ViewOutput = 'python.viewOutput'; } + +// 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 namespace LinterErrors { - export namespace pylint { - export const InvalidSyntax = 'E0001'; - } - export namespace prospector { - export const InvalidSyntax = 'F999'; - } - export namespace flake8 { - export const InvalidSyntax = 'E999'; - } -} +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 || process.env.GITHUB_ACTIONS === 'true'; export function isTestExecution(): boolean { return process.env.VSC_PYTHON_CI_TEST === '1' || isUnitTestExecution(); @@ -77,12 +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'; } +export const UseProposedApi = Symbol('USE_VSC_PROPOSED_API'); + export * from '../constants'; diff --git a/src/client/common/contextKey.ts b/src/client/common/contextKey.ts index 51d18b9431f1..96022a3ba3ce 100644 --- a/src/client/common/contextKey.ts +++ b/src/client/common/contextKey.ts @@ -1,9 +1,12 @@ import { ICommandManager } from './application/types'; export class ContextKey { - private lastValue: boolean; + public get value(): boolean | undefined { + return this.lastValue; + } + private lastValue?: boolean; - constructor(private name: string, private commandManager: ICommandManager) { } + constructor(private name: string, private commandManager: ICommandManager) {} public async set(value: boolean): Promise<void> { if (this.lastValue === value) { diff --git a/src/client/common/dotnet/compatibilityService.ts b/src/client/common/dotnet/compatibilityService.ts deleted file mode 100644 index 09a2f745342d..000000000000 --- a/src/client/common/dotnet/compatibilityService.ts +++ /dev/null @@ -1,35 +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<OSType, IDotNetCompatibilityService>(); - 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 c50ad25e3911..000000000000 --- a/src/client/common/dotnet/serviceRegistry.ts +++ /dev/null @@ -1,21 +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>(IDotNetCompatibilityService, DotNetCompatibilityService); - serviceManager.addSingleton<IOSDotNetCompatibilityService>(IOSDotNetCompatibilityService, MacDotNetCompatibilityService, OSType.OSX); - serviceManager.addSingleton<IOSDotNetCompatibilityService>(IOSDotNetCompatibilityService, WindowsDotNetCompatibilityService, OSType.Windows); - serviceManager.addSingleton<IOSDotNetCompatibilityService>(IOSDotNetCompatibilityService, LinuxDotNetCompatibilityService, OSType.Linux); - serviceManager.addSingleton<IOSDotNetCompatibilityService>(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 b53ed2bd469b..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 8d3acdb19ab8..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 1af2c180c8b2..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<boolean>; -} -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 a694984d94f7..000000000000 --- a/src/client/common/editor.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { Diff, diff_match_patch } from 'diff-match-patch'; -import * as fs from 'fs-extra'; -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 { IEditorUtils } from './types'; - -// 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): 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.existsSync(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).toString('utf8'); - 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; - if (line > 0) { - const beforeLines = before.split(/\r?\n/g); - beforeLines.filter((l, i) => i < line).forEach(l => character += l.length + NEW_LINE_LENGTH); - } - const edits: Edit[] = []; - let edit: Edit | null = null; - - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < diffs.length; i += 1) { - const 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 (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 = new Position(line, character); - 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 function getTempFileWithDocumentContents(document: TextDocument): Promise<string> { - return new Promise<string>((resolve, reject) => { - const ext = path.extname(document.uri.fsPath); - // Don't create file in temp folder since external utilities - // look into configuration files in the workspace and are not able - // 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. - - // tslint:disable-next-line:no-require-imports - const fileName = `${document.uri.fsPath}.${md5(document.uri.fsPath)}${ext}`; - fs.writeFile(fileName, document.getText(), ex => { - if (ex) { - reject(`Failed to create a temporary file, ${ex.message}`); - } - resolve(fileName); - }); - }); -} - -/** - * Parse a textual representation of patches and return a list of Patch objects. - * @param {string} textline Text representation of patches. - * @return {!Array.<!diff_match_patch.patch_obj>} Array of Patch objects. - * @throws {!Error} If invalid input. - */ -function patch_fromText(textline): 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 (<any>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/envFileParser.ts b/src/client/common/envFileParser.ts deleted file mode 100644 index f1cfda52b430..000000000000 --- a/src/client/common/envFileParser.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as fs from 'fs-extra'; -import { IS_WINDOWS } from './platform/constants'; -import { PathUtils } from './platform/pathUtils'; -import { EnvironmentVariablesService } from './variables/environment'; -import { EnvironmentVariables } from './variables/types'; -function parseEnvironmentVariables(contents: string): EnvironmentVariables | undefined { - if (typeof contents !== 'string' || contents.length === 0) { - return undefined; - } - - const env: EnvironmentVariables = {}; - contents.split('\n').forEach(line => { - const match = line.match(/^\s*([\w\.\-]+)\s*=\s*(.*)?\s*$/); - if (match !== null) { - let value = typeof match[2] === 'string' ? match[2] : ''; - if (value.length > 0 && value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') { - value = value.replace(/\\n/gm, '\n'); - } - env[match[1]] = value.replace(/(^['"]|['"]$)/g, ''); - } - }); - return env; -} - -export function parseEnvFile(envFile: string, mergeWithProcessEnvVars: boolean = true): EnvironmentVariables { - const buffer = fs.readFileSync(envFile, 'utf8'); - const env = parseEnvironmentVariables(buffer)!; - return mergeWithProcessEnvVars ? mergeEnvVariables(env, process.env) : mergePythonPath(env, process.env.PYTHONPATH as string); -} - -/** - * Merge the target environment variables into the source. - * Note: The source variables are modified and returned (i.e. it modifies value passed in). - * @export - * @param {EnvironmentVariables} targetEnvVars target environment variables. - * @param {EnvironmentVariables} [sourceEnvVars=process.env] source environment variables (defaults to current process variables). - * @returns {EnvironmentVariables} - */ -export function mergeEnvVariables(targetEnvVars: EnvironmentVariables, sourceEnvVars: EnvironmentVariables = process.env): EnvironmentVariables { - const service = new EnvironmentVariablesService(new PathUtils(IS_WINDOWS)); - service.mergeVariables(sourceEnvVars, targetEnvVars); - if (sourceEnvVars.PYTHONPATH) { - service.appendPythonPath(targetEnvVars, sourceEnvVars.PYTHONPATH); - } - return targetEnvVars; -} - -/** - * Merge the target PYTHONPATH value into the env variables passed. - * Note: The env variables passed in are modified and returned (i.e. it modifies value passed in). - * @export - * @param {EnvironmentVariables} env target environment variables. - * @param {string | undefined} [currentPythonPath] PYTHONPATH value. - * @returns {EnvironmentVariables} - */ -export function mergePythonPath(env: EnvironmentVariables, currentPythonPath: string | undefined): EnvironmentVariables { - if (typeof currentPythonPath !== 'string' || currentPythonPath.length === 0) { - return env; - } - const service = new EnvironmentVariablesService(new PathUtils(IS_WINDOWS)); - service.appendPythonPath(env, currentPythonPath); - return env; -} diff --git a/src/client/common/errors/errorUtils.ts b/src/client/common/errors/errorUtils.ts index e039ed870c83..7867d5ccfe30 100644 --- a/src/client/common/errors/errorUtils.ts +++ b/src/client/common/errors/errorUtils.ts @@ -1,9 +1,21 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// tslint:disable-next-line:no-stateless-class export class ErrorUtils { public static outputHasModuleNotInstalledError(moduleName: string, content?: string): boolean { - return content && (content!.indexOf(`No module named ${moduleName}`) > 0 || content!.indexOf(`No module named '${moduleName}'`) > 0); + return content && + (content!.indexOf(`No module named ${moduleName}`) > 0 || + content!.indexOf(`No module named '${moduleName}'`) > 0) + ? true + : false; + } +} + +/** + * An error class that contains a telemetry safe reason. + */ +export class ErrorWithTelemetrySafeReason extends Error { + constructor(message: string, public readonly telemetrySafeReason: string) { + super(message); } } diff --git a/src/client/common/experiments/groups.ts b/src/client/common/experiments/groups.ts new file mode 100644 index 000000000000..12f4ef89018b --- /dev/null +++ b/src/client/common/experiments/groups.ts @@ -0,0 +1,21 @@ +// Experiment to check whether to show "Extension Survey prompt" or not. +export enum ShowExtensionSurveyPrompt { + experiment = 'pythonSurveyNotification', +} + +export enum ShowToolsExtensionPrompt { + experiment = 'pythonPromptNewToolsExt', +} + +export enum TerminalEnvVarActivation { + experiment = 'pythonTerminalEnvVarActivation', +} + +export enum DiscoveryUsingWorkers { + experiment = 'pythonDiscoveryUsingWorkers', +} + +// 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/service.ts b/src/client/common/experiments/service.ts new file mode 100644 index 000000000000..e52773004fb3 --- /dev/null +++ b/src/client/common/experiments/service.ts @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +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, 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 { + /** + * 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; + + constructor( + @inject(IWorkspaceService) readonly workspaceService: IWorkspaceService, + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, + @inject(IPersistentStateFactory) private readonly persistentState: IPersistentStateFactory, + ) { + const settings = this.workspaceService.getConfiguration('python'); + // Users can only opt in or out of experiment groups, not control groups. + const optInto = settings.get<string[]>('experiments.optInto') || []; + const optOutFrom = settings.get<string[]>('experiments.optOutFrom') || []; + this._optInto = optInto.filter((exp) => !exp.endsWith('control')); + this._optOutFrom = optOutFrom.filter((exp) => !exp.endsWith('control')); + + // 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<boolean>('experiments.enabled') === false) { + this.enabled = false; + } else { + this.enabled = true; + } + + if (!this.enabled) { + return; + } + + let targetPopulation: TargetPopulation; + // if running in VS Code Insiders, use the Insiders target population + if (this.appEnvironment.channel === 'insiders') { + targetPopulation = TargetPopulation.Insiders; + } else { + targetPopulation = TargetPopulation.Public; + } + + const telemetryReporter = new ExperimentationTelemetry(); + + this.experimentationService = getExperimentationService( + PVSC_EXTENSION_ID, + this.appEnvironment.packageJson.version!, + targetPopulation, + telemetryReporter, + this.experiments.storage, + ); + } + + public async activate(): Promise<void> { + 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. + + // 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<boolean> { + 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 manually. + if (this._optOutFrom.includes('All') || this._optOutFrom.includes(experiment)) { + return false; + } + + if (this._optInto.includes('All') || this._optInto.includes(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; + } + + // 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<T extends boolean | number | string>(experiment: string): Promise<T | undefined> { + if (!this.experimentationService || this._optOutFrom.includes('All') || this._optOutFrom.includes(experiment)) { + return undefined; + } + + return this.experimentationService.getTreatmentVariable<T>(EXP_CONFIG_ID, experiment); + } + + private logExperiments() { + const telemetrySettings = this.workspaceService.getConfiguration('telemetry'); + let experimentsDisabled = false; + if (telemetrySettings && telemetrySettings.get<boolean>('enableTelemetry') === false) { + traceLog('Telemetry is disabled'); + experimentsDisabled = true; + } + + if (telemetrySettings && telemetrySettings.get<string>('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: + * <root> -> "contributes" -> "configuration" -> "properties" -> <setting name> -> "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, unknown>): 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<string, unknown>): 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 new file mode 100644 index 000000000000..bcc9a9c02005 --- /dev/null +++ b/src/client/common/experiments/telemetry.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IExperimentationTelemetry } from 'vscode-tas-client'; +import { sendTelemetryEvent, setSharedProperty } from '../../telemetry'; + +export class ExperimentationTelemetry implements IExperimentationTelemetry { + public setSharedProperty(name: string, value: string): void { + // Add the shared property to all telemetry being sent, not just events being sent by the experimentation package. + // We are not in control of these props, just cast to `any`, i.e. we cannot strongly type these external props. + + setSharedProperty(name as any, value as any); + } + + public postEvent(eventName: string, properties: Map<string, string>): void { + const formattedProperties: { [key: string]: string } = {}; + properties.forEach((value, key) => { + formattedProperties[key] = value; + }); + + sendTelemetryEvent(eventName as any, undefined, formattedProperties); + } +} diff --git a/src/client/common/extensions.ts b/src/client/common/extensions.ts index df8bc0981938..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,75 +26,53 @@ 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, '/'); }; /** * String.trimQuotes implementation * Removes leading and trailing quotes from a string */ -String.prototype.trimQuotes = function (this): string { +String.prototype.trimQuotes = function (this: string): string { if (!this) { return this; } return this.replace(/(^['"])|(['"]$)/g, ''); }; -// tslint:disable-next-line:interface-name -declare interface Promise<T> { - /** - * Catches task error and ignores them. - */ - ignoreErrors(): void; -} - /** * Explicitly tells that promise should be run asynchonously. */ Promise.prototype.ignoreErrors = function <T>(this: Promise<T>) { - // tslint:disable-next-line:no-empty - this.catch(() => { }); + return this.catch(() => {}); }; if (!String.prototype.format) { String.prototype.format = function (this: string) { const args = arguments; - return this.replace(/{(\d+)}/g, (match, number) => args[number] === undefined ? match : args[number]); + return this.replace(/{(\d+)}/g, (match, number) => (args[number] === undefined ? match : args[number])); }; } diff --git a/src/client/common/featureDeprecationManager.ts b/src/client/common/featureDeprecationManager.ts deleted file mode 100644 index 6fe6905c1f11..000000000000 --- a/src/client/common/featureDeprecationManager.ts +++ /dev/null @@ -1,135 +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 { 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 the new Language Server (\'python.jediEnabled = false\').', - 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<void> { - 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 => console.error('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 d0e3ccc67070..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 isError = typeof error === 'object' && error !== null; + const errorObj = <any>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<any>) { - 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/installer/channelManager.ts b/src/client/common/installer/channelManager.ts index d145f6216b10..d2950859ab80 100644 --- a/src/client/common/installer/channelManager.ts +++ b/src/client/common/installer/channelManager.ts @@ -3,19 +3,25 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; -import { IInterpreterService, InterpreterType } from '../../interpreter/contracts'; +import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; +import { EnvironmentType } from '../../pythonEnvironments/info'; import { IApplicationShell } from '../application/types'; import { IPlatformService } from '../platform/types'; import { Product } from '../types'; +import { Installer } from '../utils/localize'; +import { isResource } from '../utils/misc'; import { ProductNames } from './productNames'; -import { IInstallationChannelManager, IModuleInstaller } from './types'; +import { IInstallationChannelManager, IModuleInstaller, InterpreterUri } from './types'; @injectable() export class InstallationChannelManager implements IInstallationChannelManager { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} - public async getInstallationChannel(product: Product, resource?: Uri): Promise<IModuleInstaller | undefined> { + public async getInstallationChannel( + product: Product, + resource?: InterpreterUri, + ): Promise<IModuleInstaller | undefined> { const channels = await this.getInstallationChannels(resource); if (channels.length === 1) { return channels[0]; @@ -24,23 +30,27 @@ export class InstallationChannelManager implements IInstallationChannelManager { const productName = ProductNames.get(product)!; const appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell); if (channels.length === 0) { - await this.showNoInstallersMessage(resource); + await this.showNoInstallersMessage(isResource(resource) ? resource : undefined); return; } const placeHolder = `Select an option to install ${productName}`; - const options = channels.map(installer => { + const options = channels.map((installer) => { return { label: `Install using ${installer.displayName}`, description: '', - installer + installer, }; }); - const selection = await appShell.showQuickPick<typeof options[0]>(options, { matchOnDescription: true, matchOnDetail: true, placeHolder }); + const selection = await appShell.showQuickPick<typeof options[0]>(options, { + matchOnDescription: true, + matchOnDetail: true, + placeHolder, + }); return selection ? selection.installer : undefined; } - public async getInstallationChannels(resource?: Uri): Promise<IModuleInstaller[]> { + public async getInstallationChannels(resource?: InterpreterUri): Promise<IModuleInstaller[]> { const installers = this.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller); const supportedInstallers: IModuleInstaller[] = []; if (installers.length === 0) { @@ -74,17 +84,19 @@ export class InstallationChannelManager implements IInstallationChannelManager { const appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell); const search = 'Search for help'; let result: string | undefined; - if (interpreter.type === InterpreterType.Conda) { - result = await appShell.showErrorMessage('There is no Conda or Pip installer available in the selected environment.', search); + if (interpreter.envType === EnvironmentType.Conda) { + result = await appShell.showErrorMessage(Installer.noCondaOrPipInstaller, Installer.searchForHelp); } else { - result = await appShell.showErrorMessage('There is no Pip installer available in the selected environment.', search); + result = await appShell.showErrorMessage(Installer.noPipInstaller, Installer.searchForHelp); } if (result === search) { const platform = this.serviceContainer.get<IPlatformService>(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' : ''}`); + const osName = platform.isWindows ? 'Windows' : platform.isMac ? 'MacOS' : 'Linux'; + appShell.openUrl( + `https://www.bing.com/search?q=Install Pip ${osName} ${ + interpreter.envType === EnvironmentType.Conda ? 'Conda' : '' + }`, + ); } } } diff --git a/src/client/common/installer/condaInstaller.ts b/src/client/common/installer/condaInstaller.ts index ddcb7b6bb9fc..fbb3dcf183ef 100644 --- a/src/client/common/installer/condaInstaller.ts +++ b/src/client/common/installer/condaInstaller.ts @@ -1,33 +1,45 @@ +/* eslint-disable class-methods-use-this */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { ICondaService } from '../../interpreter/contracts'; +import { ICondaService, IComponentAdapter } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { ExecutionInfo, IConfigurationService } from '../types'; -import { ModuleInstaller } from './moduleInstaller'; -import { IModuleInstaller } 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, translateProductToModule } from './moduleInstaller'; +import { InterpreterUri, ModuleInstallFlags } from './types'; /** * A Python module installer for a conda environment. */ @injectable() -export class CondaInstaller extends ModuleInstaller implements IModuleInstaller { - private isCondaAvailable: boolean | undefined; +export class CondaInstaller extends ModuleInstaller { + public _isCondaAvailable: boolean | undefined; - constructor( - @inject(IServiceContainer) serviceContainer: IServiceContainer - ) { + // 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); } - public get displayName() { + public get name(): string { return 'Conda'; } + public get displayName(): string { + return 'Conda'; + } + + public get type(): ModuleInstallerType { + return ModuleInstallerType.Conda; + } + public get priority(): number { - return 0; + return 10; } /** @@ -35,16 +47,16 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller * We need to perform two checks: * 1. Ensure we have conda. * 2. Check if the current environment is a conda environment. - * @param {Uri} [resource=] Resource used to identify the workspace. + * @param {InterpreterUri} [resource=] Resource used to identify the workspace. * @returns {Promise<boolean>} Whether conda is supported as a module installer or not. */ - public async isSupported(resource?: Uri): Promise<boolean> { - if (this.isCondaAvailable === false) { + public async isSupported(resource?: InterpreterUri): Promise<boolean> { + if (this._isCondaAvailable === false) { return false; } const condaLocator = this.serviceContainer.get<ICondaService>(ICondaService); - this.isCondaAvailable = await condaLocator.isCondaAvailable(); - if (!this.isCondaAvailable) { + this._isCondaAvailable = await condaLocator.isCondaAvailable(); + if (!this._isCondaAvailable) { return false; } // Now we need to check if the current environment is a conda environment or not. @@ -54,36 +66,63 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller /** * Return the commandline args needed to install the module. */ - protected async getExecutionInfo(moduleName: string, resource?: Uri): Promise<ExecutionInfo> { + protected async getExecutionInfo( + moduleName: string, + resource?: InterpreterUri, + flags: ModuleInstallFlags = 0, + ): Promise<ExecutionInfo> { const condaService = this.serviceContainer.get<ICondaService>(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 = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath; - const info = await condaService.getCondaEnvironment(pythonPath); - const args = ['install']; + const pythonPath = isResource(resource) + ? this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath + : getEnvPath(resource.path, resource.envPath).path ?? ''; + const condaLocatorService = this.serviceContainer.get<IComponentAdapter>(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, }; } /** - * Is anaconda the current interpreter? + * Is the provided interprter a conda environment */ - private async isCurrentEnvironmentACondaEnvironment(resource?: Uri): Promise<boolean> { - const condaService = this.serviceContainer.get<ICondaService>(ICondaService); - const pythonPath = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath; + private async isCurrentEnvironmentACondaEnvironment(resource?: InterpreterUri): Promise<boolean> { + const condaService = this.serviceContainer.get<IComponentAdapter>(IComponentAdapter); + const pythonPath = isResource(resource) + ? this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath + : getEnvPath(resource.path, resource.envPath).path ?? ''; return condaService.isCondaEnvironment(pythonPath); } } diff --git a/src/client/common/installer/moduleInstaller.ts b/src/client/common/installer/moduleInstaller.ts index 6dc2a6f8b7f3..9dacb623c606 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -1,109 +1,261 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as fs from 'fs'; import { injectable } from 'inversify'; import * as path from 'path'; -import * as vscode from 'vscode'; -import { IInterpreterService, InterpreterType } from '../../interpreter/contracts'; +import { CancellationToken, l10n, ProgressLocation, ProgressOptions } from 'vscode'; +import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../constants'; -import { ITerminalServiceFactory } from '../terminal/types'; -import { ExecutionInfo, IConfigurationService, IOutputChannel } from '../types'; -import { noop } from '../utils/misc'; +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 { IFileSystem } from '../platform/types'; +import * as internalPython from '../process/internal/python'; +import { IProcessServiceFactory } from '../process/types'; +import { ITerminalServiceFactory, TerminalCreationOptions } from '../terminal/types'; +import { ExecutionInfo, IConfigurationService, ILogOutputChannel, Product } from '../types'; +import { isResource } from '../utils/misc'; +import { ProductNames } from './productNames'; +import { IModuleInstaller, InstallOptions, InterpreterUri, ModuleInstallFlags } from './types'; @injectable() -export abstract class ModuleInstaller { - constructor(protected serviceContainer: IServiceContainer) { } - public async installModule(name: string, resource?: vscode.Uri): Promise<void> { - const executionInfo = await this.getExecutionInfo(name, resource); - const terminalService = this.serviceContainer.get<ITerminalServiceFactory>(ITerminalServiceFactory).getTerminalService(resource); - - const executionInfoArgs = await this.processInstallArgs(executionInfo.args, resource); - if (executionInfo.moduleName) { - const configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); - const settings = configService.getSettings(resource); - const args = ['-m', executionInfo.moduleName].concat(executionInfoArgs); - - const pythonPath = settings.pythonPath; - - const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService); - const currentInterpreter = await interpreterService.getActiveInterpreter(resource); - - if (!currentInterpreter || currentInterpreter.type !== InterpreterType.Unknown) { - await terminalService.sendCommand(pythonPath, args); - } else if (settings.globalModuleInstallation) { - if (await this.isPathWritableAsync(path.dirname(pythonPath))) { - await terminalService.sendCommand(pythonPath, args); +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( + productOrModuleName: Product | string, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, + ): Promise<void> { + 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, flags); + + const install = async (token?: CancellationToken) => { + const executionInfoArgs = await this.processInstallArgs(executionInfo.args, resource); + if (executionInfo.moduleName) { + const configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); + const settings = configService.getSettings(uri); + + const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService); + const interpreter = isResource(resource) + ? await interpreterService.getActiveInterpreter(resource) + : resource; + const interpreterPath = interpreter?.path ?? settings.pythonPath; + const pythonPath = isResource(resource) ? interpreterPath : resource.path; + const args = internalPython.execModule(executionInfo.moduleName, executionInfoArgs); + 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>(IFileSystem); + if (await fs.isDirReadonly(path.dirname(pythonPath)).catch((_err) => true)) { + this.elevatedInstall(pythonPath, args); + } else { + 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 { - this.elevatedInstall(pythonPath, args); + await this.executeCommand( + shouldExecuteInTerminal, + resource, + pythonPath, + args.concat(['--user']), + token, + executionInfo.useShell, + ); } } else { - await terminalService.sendCommand(pythonPath, args.concat(['--user'])); + 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 && !options?.hideProgress) { + const shell = this.serviceContainer.get<IApplicationShell>(IApplicationShell); + const options: ProgressOptions = { + location: ProgressLocation.Notification, + cancellable: true, + title: l10n.t('Installing {0}', name), + }; + await shell.withProgress(options, async (_, token: CancellationToken) => + install(wrapCancellationTokens(token, cancel)), + ); } else { - await terminalService.sendCommand(executionInfo.execPath!, executionInfoArgs); + await install(cancel); } } - public abstract isSupported(resource?: vscode.Uri): Promise<boolean>; - protected abstract getExecutionInfo(moduleName: string, resource?: vscode.Uri): Promise<ExecutionInfo>; - private async processInstallArgs(args: string[], resource?: vscode.Uri): Promise<string[]> { - const indexOfPylint = args.findIndex(arg => arg.toUpperCase() === 'PYLINT'); - if (indexOfPylint === -1) { - return args; - } - // If installing pylint on python 2.x, then use pylint~=1.9.0 - const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService); - const currentInterpreter = await interpreterService.getActiveInterpreter(resource); - if (currentInterpreter && currentInterpreter.version && currentInterpreter.version.major === 2) { - const newArgs = [...args]; - // This command could be sent to the terminal, hence '<' needs to be escaped for UNIX. - newArgs[indexOfPylint] = '"pylint<2.0.0"'; - return newArgs; - } - return args; - } - private async isPathWritableAsync(directoryPath: string): Promise<boolean> { - const filePath = `${directoryPath}${path.sep}___vscpTest___`; - return new Promise<boolean>(resolve => { - fs.open(filePath, fs.constants.O_CREAT | fs.constants.O_RDWR, (error, fd) => { - if (!error) { - fs.close(fd, (e) => { - fs.unlink(filePath, noop); - }); - } - return resolve(!error); - }); - }); - } + public abstract isSupported(resource?: InterpreterUri): Promise<boolean>; - private elevatedInstall(execPath: string, args: string[]) { + protected elevatedInstall(execPath: string, args: string[]) { const options = { - name: 'VS Code Python' + name: 'VS Code Python', }; - const outputChannel = this.serviceContainer.get<vscode.OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + const outputChannel = this.serviceContainer.get<ILogOutputChannel>(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, (error, stdout, stderr) => { + sudo.exec(command, options, async (error: string, stdout: string, stderr: string) => { if (error) { - vscode.window.showErrorMessage(error); + const shell = this.serviceContainer.get<IApplicationShell>(IApplicationShell); + await shell.showErrorMessage(error); } 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, + flags?: ModuleInstallFlags, + ): Promise<ExecutionInfo>; + + private async processInstallArgs(args: string[], resource?: InterpreterUri): Promise<string[]> { + const indexOfPylint = args.findIndex((arg) => arg.toUpperCase() === 'PYLINT'); + if (indexOfPylint === -1) { + return args; + } + const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService); + const interpreter = isResource(resource) ? await interpreterService.getActiveInterpreter(resource) : resource; + // If installing pylint on python 2.x, then use pylint~=1.9.0 + if (interpreter && interpreter.version && interpreter.version.major === 2) { + const newArgs = [...args]; + // This command could be sent to the terminal, hence '<' needs to be escaped for UNIX. + newArgs[indexOfPylint] = '"pylint<2.0.0"'; + return newArgs; + } + return args; + } + + 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>(ITerminalServiceFactory) + .getTerminalService(options); + + terminalService.sendCommand(command, args, token); + } else { + const processServiceFactory = this.serviceContainer.get<IProcessServiceFactory>(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 2833aa218324..2c7dece6a298 100644 --- a/src/client/common/installer/pipEnvInstaller.ts +++ b/src/client/common/installer/pipEnvInstaller.ts @@ -2,18 +2,27 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { IInterpreterLocatorService, PIPENV_SERVICE } from '../../interpreter/contracts'; +import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; +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 { IModuleInstaller } from './types'; +import { InterpreterUri, ModuleInstallFlags } from './types'; export const pipenvName = 'pipenv'; @injectable() -export class PipEnvInstaller extends ModuleInstaller implements IModuleInstaller { - private readonly pipenv: IInterpreterLocatorService; +export class PipEnvInstaller extends ModuleInstaller { + public get name(): string { + return 'pipenv'; + } + + public get type(): ModuleInstallerType { + return ModuleInstallerType.Pipenv; + } public get displayName() { return pipenvName; @@ -24,16 +33,38 @@ export class PipEnvInstaller extends ModuleInstaller implements IModuleInstaller constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super(serviceContainer); - this.pipenv = this.serviceContainer.get<IInterpreterLocatorService>(IInterpreterLocatorService, PIPENV_SERVICE); } - public async isSupported(resource?: Uri): Promise<boolean> { - const interpreters = await this.pipenv.getInterpreters(resource); - return interpreters && interpreters.length > 0; + public async isSupported(resource?: InterpreterUri): Promise<boolean> { + if (isResource(resource)) { + const interpreter = await this.serviceContainer + .get<IInterpreterService>(IInterpreterService) + .getActiveInterpreter(resource); + const workspaceFolder = resource + ? this.serviceContainer.get<IWorkspaceService>(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.envType === EnvironmentType.Pipenv; + } } - protected async getExecutionInfo(moduleName: string, resource?: Uri): Promise<ExecutionInfo> { + protected async getExecutionInfo( + moduleName: string, + _resource?: InterpreterUri, + flags: ModuleInstallFlags = 0, + ): Promise<ExecutionInfo> { + // 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: ['install', moduleName, '--dev'], - execPath: pipenvName + args: args, + execPath: pipenvName, }; } } diff --git a/src/client/common/installer/pipInstaller.ts b/src/client/common/installer/pipInstaller.ts index 16f886f29e93..cb0274ea5b31 100644 --- a/src/client/common/installer/pipInstaller.ts +++ b/src/client/common/installer/pipInstaller.ts @@ -2,16 +2,51 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; 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 { ModuleInstaller } from './moduleInstaller'; -import { IModuleInstaller } from './types'; +import { ExecutionInfo, IInstaller, Product } from '../types'; +import { isResource } from '../utils/misc'; +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>(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 implements IModuleInstaller { +export class PipInstaller extends ModuleInstaller { + public get name(): string { + return 'Pip'; + } + + public get type(): ModuleInstallerType { + return ModuleInstallerType.Pip; + } + public get displayName() { return 'Pip'; } @@ -21,26 +56,82 @@ export class PipInstaller extends ModuleInstaller implements IModuleInstaller { constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super(serviceContainer); } - public isSupported(resource?: Uri): Promise<boolean> { + public async isSupported(resource?: InterpreterUri): Promise<boolean> { + if ((await doesEnvironmentContainPython(this.serviceContainer, resource)) === false) { + return false; + } return this.isPipAvailable(resource); } - protected async getExecutionInfo(moduleName: string, resource?: Uri): Promise<ExecutionInfo> { - const proxyArgs: string[] = []; + protected async getExecutionInfo( + moduleName: string, + resource?: InterpreterUri, + flags: ModuleInstallFlags = 0, + ): Promise<ExecutionInfo> { + 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>(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>(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>(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(resource?: Uri): Promise<boolean> { + private isPipAvailable(info?: InterpreterUri): Promise<boolean> { const pythonExecutionFactory = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); - return pythonExecutionFactory.create({ resource }) - .then(proc => proc.isModuleInstalled('pip')) + const resource = isResource(info) ? info : undefined; + const pythonPath = isResource(info) ? undefined : info.path; + return pythonExecutionFactory + .create({ resource, pythonPath }) + .then((proc) => proc.isModuleInstalled('pip')) .catch(() => false); } } diff --git a/src/client/common/installer/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<boolean> { + if (isResource(resource)) { + const interpreter = await this.serviceContainer + .get<IInterpreterService>(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<ExecutionInfo> { + 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 new file mode 100644 index 000000000000..5017d0813d98 --- /dev/null +++ b/src/client/common/installer/poetryInstaller.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { 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 { ExecutionInfo, IConfigurationService } from '../types'; +import { isResource } from '../utils/misc'; +import { ModuleInstaller } from './moduleInstaller'; +import { InterpreterUri } from './types'; + +export const poetryName = 'poetry'; + +@injectable() +export class PoetryInstaller extends ModuleInstaller { + // eslint-disable-next-line class-methods-use-this + public get name(): string { + return 'poetry'; + } + + // 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; + } + + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + ) { + super(serviceContainer); + } + + public async isSupported(resource?: InterpreterUri): Promise<boolean> { + if (!resource) { + return false; + } + if (!isResource(resource)) { + return false; + } + const interpreter = await this.serviceContainer + .get<IInterpreterService>(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<ExecutionInfo> { + const execPath = this.configurationService.getSettings(isResource(resource) ? resource : undefined).poetryPath; + const args = ['add', '--group', 'dev', moduleName]; + return { + args, + execPath, + }; + } +} diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index 8514e978c059..831eb33efbc6 100644 --- a/src/client/common/installer/productInstaller.ts +++ b/src/client/common/installer/productInstaller.ts @@ -1,53 +1,88 @@ -// tslint:disable:max-classes-per-file max-classes-per-file +/* eslint-disable max-classes-per-file */ -import { inject, injectable, named } from 'inversify'; -import * as os from 'os'; -import { OutputChannel, Uri } from 'vscode'; -import '../../common/extensions'; +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 { EnvironmentType, ModuleInstallerType, PythonEnvironment } from '../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../telemetry'; -import { LINTER_NOT_INSTALLED_PROMPT } from '../../telemetry/constants'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../application/types'; -import { Commands, STANDARD_OUTPUT_CHANNEL } from '../constants'; -import { IPlatformService } from '../platform/types'; +import { EventName } from '../../telemetry/constants'; +import { IApplicationShell, IWorkspaceService } from '../application/types'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../process/types'; -import { ITerminalServiceFactory } from '../terminal/types'; import { - IConfigurationService, IInstaller, ILogger, InstallerResponse, IOutputChannel, - IPersistentStateFactory, ModuleNamePurpose, Product, ProductType + IConfigurationService, + IInstaller, + InstallerResponse, + IPersistentStateFactory, + ProductInstallStatus, + Product, + ProductType, } from '../types'; +import { Common } from '../utils/localize'; +import { isResource, noop } from '../utils/misc'; +import { translateProductToModule } from './moduleInstaller'; import { ProductNames } from './productNames'; -import { IInstallationChannelManager, IProductPathService, IProductService } from './types'; +import { + IBaseInstaller, + IInstallationChannelManager, + IModuleInstaller, + InstallOptions, + InterpreterUri, + IProductPathService, + IProductService, + ModuleInstallFlags, +} from './types'; +import { traceError, traceInfo } from '../../logging'; +import { isParentPath } from '../platform/fs-paths'; export { Product } from '../types'; -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, Set<EnvironmentType>>([ + [Product.torchProfilerInstallName, new Set([EnvironmentType.Conda, EnvironmentType.Pixi])], +]); -export abstract class BaseInstaller { +abstract class BaseInstaller implements IBaseInstaller { private static readonly PromptPromises = new Map<string, Promise<InstallerResponse>>(); + 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>(IApplicationShell); this.configService = serviceContainer.get<IConfigurationService>(IConfigurationService); this.workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService); this.productService = serviceContainer.get<IProductService>(IProductService); + this.persistentStateFactory = serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory); } - public promptToInstall(product: Product, resource?: Uri): Promise<InstallerResponse> { + public promptToInstall( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + ): Promise<InstallerResponse> { // If this method gets called twice, while previous promise has not been resolved, then return that same promise. // E.g. previous promise is not resolved as a message has been displayed to the user, so no point displaying // another message. - const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; + const workspaceFolder = + resource && isResource(resource) ? this.workspaceService.getWorkspaceFolder(resource) : undefined; const key = `${product}${workspaceFolder ? workspaceFolder.uri.fsPath : ''}`; if (BaseInstaller.PromptPromises.has(key)) { return BaseInstaller.PromptPromises.get(key)!; } - const promise = this.promptToInstallImplementation(product, resource); + 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(); @@ -55,7 +90,13 @@ export abstract class BaseInstaller { return promise; } - public async install(product: Product, resource?: Uri): Promise<InstallerResponse> { + public async install( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, + ): Promise<InstallerResponse> { if (product === Product.unittest) { return InstallerResponse.Installed; } @@ -63,211 +104,372 @@ export abstract class BaseInstaller { const channels = this.serviceContainer.get<IInstallationChannelManager>(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); - const logger = this.serviceContainer.get<ILogger>(ILogger); - await installer.installModule(moduleName, resource) - .catch(logger.logError.bind(logger, `Error in installing the module '${moduleName}'`)); + await installer + .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; + }); + } + + /** + * + * @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<ProductInstallStatus> { + 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<semver.SemVer | null> { + const interpreter = isResource(resource) ? undefined : resource; + const uri = isResource(resource) ? resource : undefined; + const executableName = this.getExecutableNameFromSettings(product, uri); + + const isModule = this.isExecutableAModule(product, uri); - return this.isInstalled(product, resource) - .then(isInstalled => isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore); + let version; + if (isModule) { + const pythonProcess = await this.serviceContainer + .get<IPythonExecutionFactory>(IPythonExecutionFactory) + .createActivatedEnvironment({ resource: uri, interpreter, allowEnvironmentFetchExceptions: true }); + version = await pythonProcess.getModuleVersion(executableName); + } else { + const process = await this.serviceContainer.get<IProcessServiceFactory>(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?: Uri): Promise<boolean | undefined> { + public async isInstalled(product: Product, resource?: InterpreterUri): Promise<boolean> { if (product === Product.unittest) { return true; } // User may have customized the module name or provided the fully qualified path. - const executableName = this.getExecutableNameFromSettings(product, resource); + const interpreter = isResource(resource) ? undefined : resource; + const uri = isResource(resource) ? resource : undefined; + const executableName = this.getExecutableNameFromSettings(product, uri); - const isModule = this.isExecutableAModule(product, resource); + const isModule = this.isExecutableAModule(product, uri); if (isModule) { - const pythonProcess = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource }); + const pythonProcess = await this.serviceContainer + .get<IPythonExecutionFactory>(IPythonExecutionFactory) + .createActivatedEnvironment({ resource: uri, interpreter, allowEnvironmentFetchExceptions: true }); return pythonProcess.isModuleInstalled(executableName); - } else { - const process = await this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(resource); - return process.exec(executableName, ['--version'], { mergeStdOutErr: true }) - .then(() => true) - .catch(() => false); } + const process = await this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(uri); + return process + .exec(executableName, ['--version'], { mergeStdOutErr: true }) + .then(() => true) + .catch(() => false); } - protected abstract promptToInstallImplementation(product: Product, resource?: Uri): Promise<InstallerResponse>; + protected abstract promptToInstallImplementation( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + ): Promise<InstallerResponse>; + protected getExecutableNameFromSettings(product: Product, resource?: Uri): string { const productType = this.productService.getProductType(product); const productPathService = this.serviceContainer.get<IProductPathService>(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>(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<InstallerResponse> { - if (this.serviceContainer.get<IPlatformService>(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>(ITerminalServiceFactory).getTerminalService(resource); - const logger = this.serviceContainer.get<ILogger>(ILogger); - terminalService.sendCommand(CTagsInsllationScript, []) - .catch(logger.logError.bind(logger, `Failed to install ctags. Script sent '${CTagsInsllationScript}'.`)); - } - return InstallerResponse.Ignore; - } - protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise<InstallerResponse> { - 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): Promise<InstallerResponse> { - // 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)!); +export class TestFrameworkInstaller extends BaseInstaller { + protected async promptToInstallImplementation( + product: Product, + resource?: Uri, + cancel?: CancellationToken, + _flags?: ModuleInstallFlags, + ): Promise<InstallerResponse> { 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?`; + const options: string[] = []; + let message = l10n.t('Test framework {0} is not installed. Install?', productName); if (this.isExecutableAModule(product, resource)) { - options.splice(0, 0, yesChoice); + options.push(...[Common.bannerLabelYes, Common.bannerLabelNo]); } else { const executable = this.getExecutableNameFromSettings(product, resource); - message = `Path to the ${productName} formatter 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); - if (item === yesChoice) { - return this.install(product, resource); - } 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); - } - } - } - - return InstallerResponse.Ignore; + return item === Common.bannerLabelYes ? this.install(product, resource, cancel) : InstallerResponse.Ignore; } } -export class LinterInstaller extends BaseInstaller { - protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise<InstallerResponse> { - 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; +export class DataScienceInstaller extends BaseInstaller { + // Override base installer to support a more DS-friendly streamlined installation. + public async install( + product: Product, + interpreterUri?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + ): Promise<InstallerResponse> { + // Precondition + if (isResource(interpreterUri)) { + throw new Error('All data science packages require an interpreter be passed in'); } - const options = isPylint ? [selectLinter, disableInstallPrompt] : [selectLinter]; + // At this point we know that `interpreterUri` is of type PythonInterpreter + const interpreter = interpreterUri as PythonEnvironment; + + // Get a list of known installation channels, pip, conda, etc. + let channels: IModuleInstaller[] = await this.serviceContainer + .get<IInstallationChannelManager>(IInstallationChannelManager) + .getInstallationChannels(interpreter); + + // Pick an installerModule based on whether the interpreter is conda or not. Default is pip. + 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>(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>(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.`); + } + } - let message = `Linter ${productName} is not installed.`; - if (this.isExecutableAModule(product, resource)) { - options.splice(0, 0, install); + 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 { - const executable = this.getExecutableNameFromSettings(product, resource); - message = `Path to the ${productName} linter is invalid (${executable})`; + switch (interpreter.envType) { + case EnvironmentType.Pipenv: + requiredInstaller = ModuleInstallerType.Pipenv; + break; + case EnvironmentType.Poetry: + requiredInstaller = ModuleInstallerType.Poetry; + break; + default: + requiredInstaller = ModuleInstallerType.Pip; + } } - const response = await this.appShell.showErrorMessage(message, ...options); - if (response === install) { - sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { tool: productName as LinterId, action: 'install' }); - return this.install(product, resource); - } else if (response === disableInstallPrompt) { - await this.setStoredResponse(disableLinterInstallPromptKey, true); - sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { tool: productName as LinterId, action: 'disablePrompt' }); + const installerModule: IModuleInstaller | undefined = channels.find((v) => v.type === requiredInstaller); + + if (!installerModule) { + this.appShell + .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; } - if (response === selectLinter) { - sendTelemetryEvent(LINTER_NOT_INSTALLED_PROMPT, undefined, { action: 'select' }); - const commandManager = this.serviceContainer.get<ICommandManager>(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>(IPersistentStateFactory); - const state = factory.createGlobalPersistentState<boolean | undefined>(key, undefined); - return state.value === true; + await installerModule + .installModule(product, interpreter, cancel, flags) + .catch((ex) => traceError(`Error in installing the module '${moduleName}', ${ex}`)); + + 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; + }); } /** - * 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. + * This method will not get invoked for Jupyter extension. + * Implemented as a backup. */ - private async setStoredResponse(key: string, value: boolean): Promise<void> { - const factory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory); - const state = factory.createGlobalPersistentState<boolean | undefined>(key, undefined); - if (state && state.value !== value) { - await state.updateValue(value); + protected async promptToInstallImplementation( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + _flags?: ModuleInstallFlags, + ): Promise<InstallerResponse> { + const productName = ProductNames.get(product)!; + const item = await this.appShell.showErrorMessage( + l10n.t('Data Science library {0} is not installed. Install?', productName), + Common.bannerLabelYes, + Common.bannerLabelNo, + ); + if (item === Common.bannerLabelYes) { + return this.install(product, resource, cancel); } + return InstallerResponse.Ignore; } } -export class TestFrameworkInstaller extends BaseInstaller { - protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise<InstallerResponse> { - const productName = ProductNames.get(product)!; +export class PythonInstaller implements IBaseInstaller { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} - const options: string[] = []; - let message = `Test framework ${productName} is not installed. Install?`; - if (this.isExecutableAModule(product, resource)) { - options.push(...['Yes', 'No']); - } else { - const executable = this.getExecutableNameFromSettings(product, resource); - message = `Path to the ${productName} test framework is invalid (${executable})`; + public async isInstalled(product: Product, resource?: InterpreterUri): Promise<boolean> { + if (product !== Product.python) { + throw new Error(`${product} cannot be installed via conda python installer`); + } + const interpreterService = this.serviceContainer.get<IInterpreterService>(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; + } - const item = await this.appShell.showErrorMessage(message, ...options); - return item === 'Yes' ? this.install(product, resource) : InstallerResponse.Ignore; + public async install( + product: Product, + resource?: InterpreterUri, + _cancel?: CancellationToken, + _flags?: ModuleInstallFlags, + ): Promise<InstallerResponse> { + 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>(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, + ); } -} -export class RefactoringLibraryInstaller extends BaseInstaller { - protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise<InstallerResponse> { - 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) : InstallerResponse.Ignore; + // eslint-disable-next-line class-methods-use-this + public async promptToInstall( + _product: Product, + _resource?: InterpreterUri, + _cancel?: CancellationToken, + _flags?: ModuleInstallFlags, + ): Promise<InstallerResponse> { + // 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<ProductInstallStatus> { + return ProductInstallStatus.Installed; } } @@ -275,66 +477,71 @@ export class RefactoringLibraryInstaller extends BaseInstaller { export class ProductInstaller implements IInstaller { private readonly productService: IProductService; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private outputChannel: OutputChannel) { + private interpreterService: IInterpreterService; + + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { this.productService = serviceContainer.get<IProductService>(IProductService); + this.interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService); + } + + public dispose(): void { + /** Do nothing. */ + } + + public async promptToInstall( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + ): Promise<InstallerResponse> { + const currentInterpreter = isResource(resource) + ? await this.interpreterService.getActiveInterpreter(resource) + : resource; + if (!currentInterpreter) { + return InstallerResponse.Ignore; + } + return this.createInstaller(product).promptToInstall(product, resource, cancel, flags); } - // tslint:disable-next-line:no-empty - public dispose() { } - public async promptToInstall(product: Product, resource?: Uri): Promise<InstallerResponse> { - return this.createInstaller(product).promptToInstall(product, resource); + public async isProductVersionCompatible( + product: Product, + semVerRequirement: string, + resource?: InterpreterUri, + ): Promise<ProductInstallStatus> { + return this.createInstaller(product).isProductVersionCompatible(product, semVerRequirement, resource); } - public async install(product: Product, resource?: Uri): Promise<InstallerResponse> { - return this.createInstaller(product).install(product, resource); + + public async install( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, + ): Promise<InstallerResponse> { + return this.createInstaller(product).install(product, resource, cancel, flags, options); } - public async isInstalled(product: Product, resource?: Uri): Promise<boolean | undefined> { + + public async isInstalled(product: Product, resource?: InterpreterUri): Promise<boolean> { 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); + case ProductType.Python: + return new PythonInstaller(this.serviceContainer); default: break; } throw new Error(`Unknown product ${product}`); } } - -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.pep8: return 'pep8'; - 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'; - 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 f8800d347d73..00b19ce77ac3 100644 --- a/src/client/common/installer/productNames.ts +++ b/src/client/common/installer/productNames.ts @@ -3,19 +3,10 @@ import { Product } from '../types'; -// tslint:disable-next-line:variable-name export const ProductNames = new Map<Product, string>(); -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.pep8, 'pep8'); -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.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 9e4500a7eabd..b06e4b7a48a9 100644 --- a/src/client/common/installer/productPath.ts +++ b/src/client/common/installer/productPath.ts @@ -3,20 +3,16 @@ '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 '../../unittests/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() -abstract class BaseProductPathsService implements IProductPathService { +export abstract class BaseProductPathsService implements IProductPathService { protected readonly configService: IConfigurationService; protected readonly productInstaller: IInstaller; constructor(@inject(IServiceContainer) protected serviceContainer: IServiceContainer) { @@ -24,52 +20,18 @@ abstract class BaseProductPathsService implements IProductPathService { this.productInstaller = serviceContainer.get<IInstaller>(IInstaller); } public abstract getExecutableNameFromSettings(product: Product, resource?: Uri): string; - public isExecutableAModule(product: Product, resource?: Uri): Boolean { + 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 - } catch { } + moduleName = this.productInstaller.translateProductToModuleName(product); + } catch {} // User may have customized the module name or provided the fully qualifieid path. const executableName = this.getExecutableNameFromSettings(product, resource); - return typeof moduleName === 'string' && moduleName.length > 0 && path.basename(executableName) === executableName; - } -} - -@injectable() -export class CTagsProductPathService extends BaseProductPathsService { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer); - } - public getExecutableNameFromSettings(_: Product, resource?: Uri): string { - const settings = this.configService.getSettings(resource); - return settings.workspaceSymbols.ctagsPath; - } -} - -@injectable() -export class FormatterProductPathService extends BaseProductPathsService { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer); - } - public getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const settings = this.configService.getSettings(resource); - const formatHelper = this.serviceContainer.get<IFormatterHelper>(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>(ILinterManager); - return linterManager.getLinterInfo(product).pathName(resource); + return ( + typeof moduleName === 'string' && moduleName.length > 0 && path.basename(executableName) === executableName + ); } } @@ -79,23 +41,23 @@ export class TestFrameworkProductPathService extends BaseProductPathsService { super(serviceContainer); } public getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const testHelper = this.serviceContainer.get<ITestsHelper>(ITestsHelper); + const testHelper = this.serviceContainer.get<ITestingService>(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.unitTest[settingsPropNames.pathName] as string; + return settings.testing[settingsPropNames.pathName] as string; } } @injectable() -export class RefactoringLibraryProductPathService extends BaseProductPathsService { +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 a6e0c7a53c0d..bf5597cc5859 100644 --- a/src/client/common/installer/productService.ts +++ b/src/client/common/installer/productService.ts @@ -12,22 +12,14 @@ export class ProductService implements IProductService { private ProductTypes = new Map<Product, ProductType>(); 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.pep8, 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.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 5cdaaaf1ce16..1e273ada818c 100644 --- a/src/client/common/installer/serviceRegistry.ts +++ b/src/client/common/installer/serviceRegistry.ts @@ -3,28 +3,33 @@ 'use strict'; import { IServiceManager } from '../../ioc/types'; -import { IWebPanelProvider } from '../application/types'; -import { WebPanelProvider } from '../application/webPanelProvider'; import { ProductType } from '../types'; import { InstallationChannelManager } from './channelManager'; import { CondaInstaller } from './condaInstaller'; import { PipEnvInstaller } from './pipEnvInstaller'; import { PipInstaller } from './pipInstaller'; -import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from './productPath'; +import { PixiInstaller } from './pixiInstaller'; +import { PoetryInstaller } from './poetryInstaller'; +import { DataScienceProductPathService, TestFrameworkProductPathService } from './productPath'; import { ProductService } from './productService'; import { IInstallationChannelManager, IModuleInstaller, IProductPathService, IProductService } from './types'; export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PixiInstaller); serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, CondaInstaller); serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipInstaller); serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipEnvInstaller); + serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PoetryInstaller); serviceManager.addSingleton<IInstallationChannelManager>(IInstallationChannelManager, InstallationChannelManager); - serviceManager.addSingleton<IProductService>(IProductService, ProductService); - serviceManager.addSingleton<IProductPathService>(IProductPathService, CTagsProductPathService, ProductType.WorkspaceSymbols); - serviceManager.addSingleton<IProductPathService>(IProductPathService, FormatterProductPathService, ProductType.Formatter); - serviceManager.addSingleton<IProductPathService>(IProductPathService, LinterProductPathService, ProductType.Linter); - serviceManager.addSingleton<IProductPathService>(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); - serviceManager.addSingleton<IProductPathService>(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); - serviceManager.addSingleton<IWebPanelProvider>(IWebPanelProvider, WebPanelProvider); + serviceManager.addSingleton<IProductPathService>( + IProductPathService, + TestFrameworkProductPathService, + ProductType.TestFramework, + ); + serviceManager.addSingleton<IProductPathService>( + IProductPathService, + DataScienceProductPathService, + ProductType.DataScience, + ); } diff --git a/src/client/common/installer/types.ts b/src/client/common/installer/types.ts index c0521ee9386e..a85017ff0092 100644 --- a/src/client/common/installer/types.ts +++ b/src/client/common/installer/types.ts @@ -1,15 +1,55 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Uri } from 'vscode'; -import { Product, ProductType } from '../types'; +import { CancellationToken, Uri } from 'vscode'; +import { ModuleInstallerType, PythonEnvironment } from '../../pythonEnvironments/info'; +import { InstallerResponse, Product, ProductInstallStatus, ProductType, Resource } from '../types'; + +export type InterpreterUri = Resource | PythonEnvironment; export const IModuleInstaller = Symbol('IModuleInstaller'); export interface IModuleInstaller { + readonly name: string; readonly displayName: string; readonly priority: number; - installModule(name: string, resource?: Uri): Promise<void>; - isSupported(resource?: Uri): Promise<boolean>; + 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. + */ + installModule( + productOrModuleName: Product | string, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, + ): Promise<void>; + isSupported(resource?: InterpreterUri): Promise<boolean>; +} + +export const IBaseInstaller = Symbol('IBaseInstaller'); +export interface IBaseInstaller { + install( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, + ): Promise<InstallerResponse>; + promptToInstall( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + ): Promise<InstallerResponse>; + isProductVersionCompatible( + product: Product, + semVerRequirement: string, + resource?: InterpreterUri, + ): Promise<ProductInstallStatus>; + isInstalled(product: Product, resource?: InterpreterUri): Promise<boolean>; } export const IPythonInstallation = Symbol('IPythonInstallation'); @@ -19,8 +59,8 @@ export interface IPythonInstallation { export const IInstallationChannelManager = Symbol('IInstallationChannelManager'); export interface IInstallationChannelManager { - getInstallationChannel(product: Product, resource?: Uri): Promise<IModuleInstaller | undefined>; - getInstallationChannels(resource?: Uri): Promise<IModuleInstaller[]>; + getInstallationChannel(product: Product, resource?: InterpreterUri): Promise<IModuleInstaller | undefined>; + getInstallationChannels(resource?: InterpreterUri): Promise<IModuleInstaller[]>; showNoInstallersMessage(): void; } export const IProductService = Symbol('IProductService'); @@ -30,5 +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 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 new file mode 100644 index 000000000000..935d0bd89ad7 --- /dev/null +++ b/src/client/common/interpreterPathService.ts @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as fs from '../common/platform/fs-paths'; +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent, ConfigurationTarget, Event, EventEmitter, Uri } from 'vscode'; +import { traceError, traceVerbose } from '../logging'; +import { IApplicationEnvironment, IWorkspaceService } from './application/types'; +import { PythonSettings } from './configSettings'; +import { isTestExecution } from './constants'; +import { FileSystemPaths } from './platform/fs-paths'; +import { + IDisposable, + IDisposableRegistry, + IInterpreterPathService, + InspectInterpreterSettingType, + InterpreterConfigurationScope, + IPersistentState, + IPersistentStateFactory, + IPythonSettings, + Resource, +} from './types'; +import { SystemVariables } from './variables/systemVariables'; + +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(); + +export function getCIPythonPath(): string { + if (process.env.CI_PYTHON_PATH && fs.existsSync(process.env.CI_PYTHON_PATH)) { + return process.env.CI_PYTHON_PATH; + } + return 'python'; +} +@injectable() +export class InterpreterPathService implements IInterpreterPathService { + public get onDidChange(): Event<InterpreterConfigurationScope> { + return this._didChangeInterpreterEmitter.event; + } + public _didChangeInterpreterEmitter = new EventEmitter<InterpreterConfigurationScope>(); + private fileSystemPaths: FileSystemPaths; + constructor( + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IDisposableRegistry) disposables: IDisposable[], + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, + ) { + disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + this.fileSystemPaths = FileSystemPaths.withDefaults(); + } + + public async onDidChangeConfiguration(event: ConfigurationChangeEvent) { + if (event.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) { + this._didChangeInterpreterEmitter.fire({ uri: undefined, configTarget: ConfigurationTarget.Global }); + traceVerbose('Interpreter Path updated', `python.${defaultInterpreterPathSetting}`); + } + } + + public inspect(resource: Resource, useOldKey = false): InspectInterpreterSettingType { + resource = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri; + let workspaceFolderSetting: IPersistentState<string | undefined> | undefined; + let workspaceSetting: IPersistentState<string | undefined> | undefined; + if (resource) { + workspaceFolderSetting = this.persistentStateFactory.createGlobalPersistentState<string | undefined>( + this.getSettingKey(resource, ConfigurationTarget.WorkspaceFolder, useOldKey), + undefined, + ); + workspaceSetting = this.persistentStateFactory.createGlobalPersistentState<string | undefined>( + this.getSettingKey(resource, ConfigurationTarget.Workspace, useOldKey), + undefined, + ); + } + const defaultInterpreterPath: InspectInterpreterSettingType = + this.workspaceService.getConfiguration('python', resource)?.inspect<string>('defaultInterpreterPath') ?? {}; + return { + 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); + const value = + settings.workspaceFolderValue || + settings.workspaceValue || + settings.globalValue || + (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, + ): Promise<void> { + resource = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri; + if (configTarget === ConfigurationTarget.Global) { + const pythonConfig = this.workspaceService.getConfiguration('python'); + const globalValue = pythonConfig.inspect<string>('defaultInterpreterPath')!.globalValue; + if (globalValue !== pythonPath) { + await pythonConfig.update('defaultInterpreterPath', pythonPath, true); + } + return; + } + if (!resource) { + traceError('Cannot update workspace settings as no workspace is opened'); + return; + } + const settingKey = this.getSettingKey(resource, configTarget); + const persistentSetting = this.persistentStateFactory.createGlobalPersistentState<string | undefined>( + settingKey, + 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, + useOldKey = false, + ): string { + let settingKey: string; + const folderKey = this.workspaceService.getWorkspaceFolderIdentifier(resource); + if (configTarget === ConfigurationTarget.WorkspaceFolder) { + settingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${folderKey}`; + } else { + settingKey = this.workspaceService.workspaceFile + ? `WORKSPACE_INTERPRETER_PATH_${this.fileSystemPaths.normCase( + this.workspaceService.workspaceFile.fsPath, + )}` + : // Only a single folder is opened, use fsPath of the folder as key + `WORKSPACE_FOLDER_INTERPRETER_PATH_${folderKey}`; + } + if (!useOldKey && this.appEnvironment.remoteName) { + return `${this.appEnvironment.remoteName}_${settingKey}`; + } + return settingKey; + } + + public async copyOldInterpreterStorageValuesToNew(resource: Resource): Promise<void> { + resource = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri; + const oldSettings = this.inspect(resource, true); + await Promise.all([ + this._copyWorkspaceFolderValueToNewStorage(resource, oldSettings.workspaceFolderValue), + this._copyWorkspaceValueToNewStorage(resource, oldSettings.workspaceValue), + this._moveGlobalSettingValueToNewStorage(oldSettings.globalValue), + ]); + } + + public async _copyWorkspaceFolderValueToNewStorage(resource: Resource, value: string | undefined): Promise<void> { + // Copy workspace folder setting into the new storage if it hasn't been copied already + const workspaceFolderKey = this.workspaceService.getWorkspaceFolderIdentifier(resource, ''); + if (workspaceFolderKey === '') { + // No workspace folder is opened, simply return. + return; + } + const flaggedWorkspaceFolderKeysStorage = this.persistentStateFactory.createGlobalPersistentState<string[]>( + remoteWorkspaceFolderKeysForWhichTheCopyIsDone_Key, + [], + ); + const flaggedWorkspaceFolderKeys = flaggedWorkspaceFolderKeysStorage.value; + const shouldUpdateWorkspaceFolderSetting = !flaggedWorkspaceFolderKeys.includes(workspaceFolderKey); + if (shouldUpdateWorkspaceFolderSetting) { + await this.update(resource, ConfigurationTarget.WorkspaceFolder, value); + await flaggedWorkspaceFolderKeysStorage.updateValue([workspaceFolderKey, ...flaggedWorkspaceFolderKeys]); + } + } + + public async _copyWorkspaceValueToNewStorage(resource: Resource, value: string | undefined): Promise<void> { + // Copy workspace setting into the new storage if it hasn't been copied already + const workspaceKey = this.workspaceService.workspaceFile + ? this.fileSystemPaths.normCase(this.workspaceService.workspaceFile.fsPath) + : undefined; + if (!workspaceKey) { + return; + } + const flaggedWorkspaceKeysStorage = this.persistentStateFactory.createGlobalPersistentState<string[]>( + remoteWorkspaceKeysForWhichTheCopyIsDone_Key, + [], + ); + const flaggedWorkspaceKeys = flaggedWorkspaceKeysStorage.value; + const shouldUpdateWorkspaceSetting = !flaggedWorkspaceKeys.includes(workspaceKey); + if (shouldUpdateWorkspaceSetting) { + await this.update(resource, ConfigurationTarget.Workspace, value); + await flaggedWorkspaceKeysStorage.updateValue([workspaceKey, ...flaggedWorkspaceKeys]); + } + } + + public async _moveGlobalSettingValueToNewStorage(value: string | undefined) { + // Move global setting into the new storage if it hasn't been moved already + const isGlobalSettingCopiedStorage = this.persistentStateFactory.createGlobalPersistentState<boolean>( + isRemoteGlobalSettingCopiedKey, + false, + ); + const shouldUpdateGlobalSetting = !isGlobalSettingCopiedStorage.value; + if (shouldUpdateGlobalSetting) { + await this.update(undefined, ConfigurationTarget.Global, value); + await isGlobalSettingCopiedStorage.updateValue(true); + } + } +} diff --git a/src/client/common/logger.ts b/src/client/common/logger.ts deleted file mode 100644 index 1390ba63edb0..000000000000 --- a/src/client/common/logger.ts +++ /dev/null @@ -1,165 +0,0 @@ -// tslint:disable:no-console - -import { injectable } from 'inversify'; -import { skipIfTest } from './helpers'; -import { ILogger, LogLevel } from './types'; - -const PREFIX = 'Python Extension: '; - -@injectable() -export class Logger implements ILogger { - // tslint:disable-next-line:no-any - public static error(title: string = '', message: any) { - new Logger().logError(`${title}, ${message}`); - } - // tslint:disable-next-line:no-any - public static warn(title: string = '', message: any = '') { - new Logger().logWarning(`${title}, ${message}`); - } - // tslint:disable-next-line:no-any - public static verbose(title: string = '') { - new Logger().logInformation(title); - } - @skipIfTest(false) - public logError(message: string, ex?: Error) { - if (ex) { - console.error(`${PREFIX}${message}`, ex); - } else { - console.error(`${PREFIX}${message}`); - } - } - @skipIfTest(false) - public logWarning(message: string, ex?: Error) { - if (ex) { - console.warn(`${PREFIX}${message}`, ex); - } else { - console.warn(`${PREFIX}${message}`); - } - } - @skipIfTest(false) - public logInformation(message: string, ex?: Error) { - if (ex) { - console.info(`${PREFIX}${message}`, ex); - } else { - console.info(`${PREFIX}${message}`); - } - } -} - -enum LogOptions { - None = 0, - Arguments = 1, - ReturnValue = 2 -} - -// tslint:disable-next-line:no-any -function argsToLogString(args: any[]): string { - try { - return (args || []).map((item, index) => { - try { - return `Arg ${index + 1}: ${JSON.stringify(item)}`; - } catch { - return `Arg ${index + 1}: UNABLE TO DETERMINE VALUE`; - } - }).join(', '); - } catch { - return ''; - } -} - -// tslint:disable-next-line:no-any -function returnValueToLogString(returnValue: any): string { - let returnValueMessage = 'Return Value: '; - if (returnValue) { - try { - returnValueMessage += `${JSON.stringify(returnValue)}`; - } catch { - returnValueMessage += 'UNABLE TO DETERMINE VALUE'; - } - } - return returnValueMessage; -} - -export function traceVerbose(message: string) { - new Logger().logInformation(message); -} -export function traceError(message: string, ex?: Error) { - new Logger().logError(message, ex); -} -export function traceInfo(message: string) { - new Logger().logInformation(message); -} - -export namespace traceDecorators { - export function verbose(message: string) { - return trace(message, LogOptions.Arguments | LogOptions.ReturnValue); - } - export function error(message: string, ex?: Error) { - return trace(message, LogOptions.Arguments | LogOptions.ReturnValue, LogLevel.Error); - } - export function info(message: string) { - return trace(message); - } - export function warn(message: string) { - return trace(message, LogOptions.Arguments | LogOptions.ReturnValue, LogLevel.Warning); - } -} -function trace(message: string, options: LogOptions = LogOptions.None, logLevel?: LogLevel) { - // tslint:disable-next-line:no-function-expression no-any - return function (_: Object, __: string, descriptor: TypedPropertyDescriptor<any>) { - const originalMethod = descriptor.value; - // tslint:disable-next-line:no-function-expression no-any - descriptor.value = function (...args: any[]) { - // tslint:disable-next-line:no-any - function writeSuccess(returnValue?: any) { - if (logLevel === LogLevel.Error) { - return; - } - writeToLog(returnValue); - } - function writeError(ex: Error) { - writeToLog(undefined, ex); - } - // tslint:disable-next-line:no-any - function writeToLog(returnValue?: any, ex?: Error) { - const messagesToLog = [message]; - if ((options && LogOptions.Arguments) === LogOptions.Arguments) { - messagesToLog.push(argsToLogString(args)); - } - if ((options & LogOptions.ReturnValue) === LogOptions.ReturnValue) { - messagesToLog.push(returnValueToLogString(returnValue)); - } - if (ex) { - new Logger().logError(messagesToLog.join(', '), ex); - } else { - new Logger().logInformation(messagesToLog.join(', ')); - } - } - 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 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<void>) - .then(data => { - writeSuccess(data); - return data; - }) - .catch(ex => { - writeError(ex); - }); - } else { - writeSuccess(result); - } - return result; - } catch (ex) { - writeError(ex); - throw ex; - } - }; - - return descriptor; - }; -} diff --git a/src/client/common/markdown/restTextConverter.ts b/src/client/common/markdown/restTextConverter.ts deleted file mode 100644 index 2119775173fb..000000000000 --- a/src/client/common/markdown/restTextConverter.ts +++ /dev/null @@ -1,251 +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, ' ').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 65dabff655f1..115df0f2969c 100644 --- a/src/client/common/net/browser.ts +++ b/src/client/common/net/browser.ts @@ -3,14 +3,12 @@ 'use strict'; -// tslint:disable:no-require-imports no-var-requires -const opn = require('opn'); - import { injectable } from 'inversify'; +import { env, Uri } from 'vscode'; import { IBrowserService } from '../types'; export function launch(url: string) { - opn(url); + env.openExternal(Uri.parse(url)); } @injectable() diff --git a/src/client/common/net/httpClient.ts b/src/client/common/net/httpClient.ts deleted file mode 100644 index 7c8d0ffc5663..000000000000 --- a/src/client/common/net/httpClient.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 * as requestTypes from 'request'; -import { IHttpClient } from '../../activation/types'; -import { IServiceContainer } from '../../ioc/types'; -import { IWorkspaceService } from '../application/types'; - -@injectable() -export class HttpClient implements IHttpClient { - public readonly requestOptions: requestTypes.CoreOptions; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - const workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService); - this.requestOptions = { proxy: workspaceService.getConfiguration('http').get('proxy', '') }; - } - - public async downloadFile(uri: string): Promise<requestTypes.Request> { - // tslint:disable-next-line:no-any - const request = await import('request') as any as typeof requestTypes; - return request(uri, this.requestOptions); - } - public async getJSON<T>(uri: string): Promise<T> { - // tslint:disable-next-line:no-require-imports - const request = require('request') as typeof requestTypes; - return new Promise<T>((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(JSON.parse(body) as T); - }); - }); - } -} diff --git a/src/client/common/net/socket/SocketStream.ts b/src/client/common/net/socket/SocketStream.ts index f17272b36daa..b046cdceaf96 100644 --- a/src/client/common/net/socket/SocketStream.ts +++ b/src/client/common/net/socket/SocketStream.ts @@ -1,8 +1,14 @@ -"use strict"; +'use strict'; -import * as net from "net"; -const uint64be = require("uint64be"); +import * as net from 'net'; +const uint64be = require('uint64be'); + +enum DataType { + string, + int32, + int64, +} export class SocketStream { constructor(socket: net.Socket, buffer: Buffer) { @@ -16,11 +22,11 @@ export class SocketStream { } public WriteInt64(num: number) { - let buffer = uint64be.encode(num); + const buffer = uint64be.encode(num); this.socket.write(buffer); } public WriteString(value: string) { - let 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); @@ -30,9 +36,8 @@ export class SocketStream { this.socket.write(buffer); } - private buffer: Buffer; - private isInTransaction: boolean; + private isInTransaction!: boolean; private bytesRead: number = 0; public get Buffer(): Buffer { return this.buffer; @@ -76,14 +81,14 @@ export class SocketStream { this.buffer = additionalData; return; } - let 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; } private isSufficientDataAvailable(length: number): boolean { - if (this.buffer.length < (this.bytesRead + length)) { + if (this.buffer.length < this.bytesRead + length) { this.hasInsufficientDataForReading = true; } @@ -92,65 +97,62 @@ export class SocketStream { public ReadByte(): number { if (!this.isSufficientDataAvailable(1)) { - return null; + return null as any; } - let value = this.buffer.slice(this.bytesRead, this.bytesRead + 1)[0]; + const value = this.buffer.slice(this.bytesRead, this.bytesRead + 1)[0]; if (this.isInTransaction) { - this.bytesRead++; - } - else { + this.bytesRead += 1; + } else { this.buffer = this.buffer.slice(1); } return value; } public ReadString(): string { - let byteRead = this.ReadByte(); + const byteRead = this.ReadByte(); if (this.HasInsufficientDataForReading) { - return null; + return null as any; } if (byteRead < 0) { - throw new Error("IOException() - Socket.ReadString failed to read string type;"); + throw new Error('IOException() - Socket.ReadString failed to read string type;'); } - let type = new Buffer([byteRead]).toString(); + const type = Buffer.from([byteRead]).toString(); let isUnicode = false; switch (type) { - case "N": // null string - return null; - case "U": + case 'N': // null string + return null as any; + case 'U': isUnicode = true; break; - case "A": { + case 'A': { isUnicode = false; break; } default: { - throw new Error("IOException(); Socket.ReadString failed to parse unknown string type " + type); + throw new Error(`IOException(); Socket.ReadString failed to parse unknown string type ${type}`); } } - let len = this.ReadInt32(); + const len = this.ReadInt32(); if (this.HasInsufficientDataForReading) { - return null; + return null as any; } if (!this.isSufficientDataAvailable(len)) { - return null; + return null as any; } - let stringBuffer = this.buffer.slice(this.bytesRead, this.bytesRead + len); + const stringBuffer = this.buffer.slice(this.bytesRead, this.bytesRead + len); if (this.isInTransaction) { this.bytesRead = this.bytesRead + len; - } - else { + } else { this.buffer = this.buffer.slice(len); } - let resp = isUnicode ? stringBuffer.toString("utf-8") : stringBuffer.toString(); - return resp; + return isUnicode ? stringBuffer.toString('utf-8') : stringBuffer.toString(); } public ReadInt32(): number { @@ -159,35 +161,32 @@ export class SocketStream { public ReadInt64(): number { if (!this.isSufficientDataAvailable(8)) { - return null; + return null as any; } - let buf = this.buffer.slice(this.bytesRead, this.bytesRead + 8); + const buf = this.buffer.slice(this.bytesRead, this.bytesRead + 8); if (this.isInTransaction) { this.bytesRead = this.bytesRead + 8; - } - else { + } else { this.buffer = this.buffer.slice(8); } - let returnValue = uint64be.decode(buf); - return returnValue; + return uint64be.decode(buf); } public ReadAsciiString(length: number): string { if (!this.isSufficientDataAvailable(length)) { - return null; + return null as any; } - let stringBuffer = this.buffer.slice(this.bytesRead, this.bytesRead + length); + const stringBuffer = this.buffer.slice(this.bytesRead, this.bytesRead + length); if (this.isInTransaction) { this.bytesRead = this.bytesRead + length; - } - else { + } else { this.buffer = this.buffer.slice(length); } - return stringBuffer.toString("ascii"); + return stringBuffer.toString('ascii'); } private readValueInTransaction<T>(dataType: DataType): T { @@ -210,12 +209,15 @@ export class SocketStream { data = this.ReadInt64(); break; } + default: { + break; + } } if (this.HasInsufficientDataForReading) { if (startedTransaction) { this.RollBackTransaction(); } - return undefined; + return undefined as any; } if (startedTransaction) { this.EndTransaction(); @@ -234,9 +236,3 @@ export class SocketStream { return this.readValueInTransaction<number>(DataType.int64); } } - -enum DataType { - string, - int32, - int64 -} diff --git a/src/client/common/net/socket/socketCallbackHandler.ts b/src/client/common/net/socket/socketCallbackHandler.ts index dda2a4d4cf92..82fe3ec1ae0d 100644 --- a/src/client/common/net/socket/socketCallbackHandler.ts +++ b/src/client/common/net/socket/socketCallbackHandler.ts @@ -1,23 +1,21 @@ -// tslint:disable:quotemark ordered-imports member-ordering one-line prefer-const +'use strict'; -"use strict"; - -import * as net from "net"; -import { EventEmitter } from "events"; -import { SocketStream } from "./SocketStream"; +import * as net from 'net'; +import { EventEmitter } from 'events'; +import { SocketStream } from './SocketStream'; import { SocketServer } from './socketServer'; export abstract class SocketCallbackHandler extends EventEmitter { - private _stream: SocketStream; + private _stream!: SocketStream; private commandHandlers: Map<string, Function>; - private handeshakeDone: boolean; + private handeshakeDone!: boolean; constructor(socketServer: SocketServer) { super(); this.commandHandlers = new Map<string, Function>(); socketServer.on('data', this.onData.bind(this)); } - private disposed: boolean; + private disposed!: boolean; public dispose() { this.disposed = true; this.commandHandlers.clear(); @@ -46,8 +44,7 @@ export abstract class SocketCallbackHandler extends EventEmitter { private HandleIncomingData(buffer: Buffer, socket: net.Socket): boolean | undefined { if (!this._stream) { this._stream = new SocketStream(socket, buffer); - } - else { + } else { this._stream.Append(buffer); } @@ -76,9 +73,8 @@ export abstract class SocketCallbackHandler extends EventEmitter { if (this.commandHandlers.has(cmd)) { const handler = this.commandHandlers.get(cmd)!; handler(); - } - else { - this.emit("error", `Unhandled command '${cmd}'`); + } else { + this.emit('error', `Unhandled command '${cmd}'`); } if (this.stream.HasInsufficientDataForReading) { diff --git a/src/client/common/net/socket/socketServer.ts b/src/client/common/net/socket/socketServer.ts index 379fd9918538..c0e13a412d7d 100644 --- a/src/client/common/net/socket/socketServer.ts +++ b/src/client/common/net/socket/socketServer.ts @@ -20,11 +20,12 @@ export class SocketServer extends EventEmitter implements ISocketServer { this.Stop(); } public Stop() { - if (!this.socketServer) { return; } + if (!this.socketServer) { + return; + } try { this.socketServer.close(); - // tslint:disable-next-line:no-empty - } catch (ex) { } + } catch (ex) {} this.socketServer = undefined; } @@ -34,14 +35,13 @@ export class SocketServer extends EventEmitter implements ISocketServer { const port = typeof options.port === 'number' ? options.port! : 0; const host = typeof options.host === 'string' ? options.host! : 'localhost'; - this.socketServer!.on('error', ex => { - console.error('Error in Socket Server', ex); + this.socketServer!.on('error', (ex) => { const msg = `Failed to start the socket server. (Error: ${ex.message})`; def.reject(msg); }); this.socketServer!.listen({ port, host }, () => { - def.resolve(this.socketServer!.address().port); + def.resolve((this.socketServer!.address() as net.AddressInfo).port); }); return def.promise; @@ -57,9 +57,9 @@ export class SocketServer extends EventEmitter implements ISocketServer { client.on('data', (data: Buffer) => { this.emit('data', client, data); }); - client.on('error', (err: Error) => noop); + client.on('error', () => noop); - client.on('timeout', d => { + client.on('timeout', () => { // let msg = "Debugger client timedout, " + d; }); } diff --git a/src/client/common/nuget/azureBlobStoreNugetRepository.ts b/src/client/common/nuget/azureBlobStoreNugetRepository.ts deleted file mode 100644 index 3bf649da52c0..000000000000 --- a/src/client/common/nuget/azureBlobStoreNugetRepository.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, unmanaged } from 'inversify'; -import { IServiceContainer } from '../../ioc/types'; -import { captureTelemetry } from '../../telemetry'; -import { PYTHON_LANGUAGE_SERVER_LIST_BLOB_STORE_PACKAGES } from '../../telemetry/constants'; -import { traceDecorators } from '../logger'; -import { INugetRepository, INugetService, NugetPackage } from './types'; - -@injectable() -export class AzureBlobStoreNugetRepository implements INugetRepository { - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @unmanaged() protected readonly azureBlobStorageAccount: string, - @unmanaged() protected readonly azureBlobStorageContainer: string, - @unmanaged() protected readonly azureCDNBlobStorageAccount: string) { } - public async getPackages(packageName: string): Promise<NugetPackage[]> { - return this.listPackages(this.azureBlobStorageAccount, this.azureBlobStorageContainer, packageName, this.azureCDNBlobStorageAccount); - } - - @captureTelemetry(PYTHON_LANGUAGE_SERVER_LIST_BLOB_STORE_PACKAGES) - @traceDecorators.verbose('Listing Nuget Packages') - protected async listPackages(azureBlobStorageAccount: string, azureBlobStorageContainer: string, packageName: string, azureCDNBlobStorageAccount: string) { - // tslint:disable-next-line:no-require-imports - const az = await import('azure-storage') as typeof import('azure-storage'); - const blobStore = az.createBlobServiceAnonymous(azureBlobStorageAccount); - const nugetService = this.serviceContainer.get<INugetService>(INugetService); - return new Promise<NugetPackage[]>((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.map(item => { - return { - package: item.name, - uri: `${azureCDNBlobStorageAccount}/${azureBlobStorageContainer}/${item.name}`, - version: nugetService.getVersionFromPackageFileName(item.name) - }; - })); - }); - }); - } -} diff --git a/src/client/common/nuget/nugetRepository.ts b/src/client/common/nuget/nugetRepository.ts deleted file mode 100644 index e1fef9da769c..000000000000 --- a/src/client/common/nuget/nugetRepository.ts +++ /dev/null @@ -1,33 +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 '../../activation/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<NugetPackage[]> { - 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<SemVer[]> { - const uri = `${packageBaseAddress}/${packageName.toLowerCase().trim()}/index.json`; - const httpClient = this.serviceContainer.get<IHttpClient>(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 c74d0a3f0347..000000000000 --- a/src/client/common/nuget/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { SemVer } from 'semver'; -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): Promise<NugetPackage[]>; -} diff --git a/src/client/common/open.ts b/src/client/common/open.ts deleted file mode 100644 index 28efbb9afdd5..000000000000 --- a/src/client/common/open.ts +++ /dev/null @@ -1,79 +0,0 @@ -'use strict'; - -//https://github.com/sindresorhus/opn/blob/master/index.js -//Modified as this uses target as an argument - -import * as path from 'path'; -import * as childProcess from 'child_process'; - -export function open(opts: any): Promise<childProcess.ChildProcess> { - // opts = objectAssign({wait: true}, opts); - if (!opts.hasOwnProperty("wait")) { - (<any>opts).wait = true; - } - - var cmd; - var appArgs = []; - var args = []; - var 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(" ")}`] - } - - var 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); -}; \ No newline at end of file diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts index 30a479f67fc4..3f9c17657cf4 100644 --- a/src/client/common/persistentState.ts +++ b/src/client/common/persistentState.ts @@ -3,12 +3,72 @@ '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'; -export class PersistentState<T> implements IPersistentState<T>{ - constructor(private storage: Memento, private key: string, private defaultValue?: T, private expiryDurationMs?: number) { } +let _workspaceState: Memento | undefined; +const _workspaceKeys: string[] = []; +export function initializePersistentStateForTriggers(context: IExtensionContext) { + _workspaceState = context.workspaceState; +} + +export function getWorkspaceStateValue<T>(key: string, defaultValue?: T): T | undefined { + if (!_workspaceState) { + throw new Error('Workspace state not initialized'); + } + if (defaultValue === undefined) { + return _workspaceState.get<T>(key); + } + return _workspaceState.get<T>(key, defaultValue); +} + +export async function updateWorkspaceStateValue<T>(key: string, value: T): Promise<void> { + 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<void> { + if (_workspaceState !== undefined) { + await Promise.all(_workspaceKeys.map((key) => updateWorkspaceStateValue(key, undefined))); + } +} + +export class PersistentState<T> implements IPersistentState<T> { + constructor( + public readonly storage: Memento, + private key: string, + private defaultValue?: T, + private expiryDurationMs?: number, + ) {} public get value(): T { if (this.expiryDurationMs) { @@ -23,23 +83,156 @@ export class PersistentState<T> implements IPersistentState<T>{ } } - public async updateValue(newValue: T): Promise<void> { - 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<void> { + 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 { - constructor(@inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, - @inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceState: Memento) { } - public createGlobalPersistentState<T>(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState<T> { +export class PersistentStateFactory implements IPersistentStateFactory, IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + public readonly _globalKeysStorage = new PersistentState<KeysStorage[]>( + this.globalState, + GLOBAL_PERSISTENT_KEYS, + [], + ); + public readonly _workspaceKeysStorage = new PersistentState<KeysStorage[]>( + this.workspaceState, + WORKSPACE_PERSISTENT_KEYS, + [], + ); + constructor( + @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, + @inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceState: Memento, + @inject(ICommandManager) private cmdManager?: ICommandManager, + @inject(IExtensionContext) @optional() private context?: IExtensionContext, + ) {} + + public async activate(): Promise<void> { + 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<T>( + key: string, + defaultValue?: T, + expiryDurationMs?: number, + ): IPersistentState<T> { + this.addKeyToStorage('global', key, defaultValue).ignoreErrors(); return new PersistentState<T>(this.globalState, key, defaultValue, expiryDurationMs); } - public createWorkspacePersistentState<T>(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState<T> { + + public createWorkspacePersistentState<T>( + key: string, + defaultValue?: T, + expiryDurationMs?: number, + ): IPersistentState<T> { + this.addKeyToStorage('workspace', key, defaultValue).ignoreErrors(); return new PersistentState<T>(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<T>(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<void> { + 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<T> { + get(): T; + set(value: T): Promise<void>; +} + +/** + * Build a global storage object for the given key. + */ +export function getGlobalStorage<T>(context: IExtensionContext, key: string, defaultValue?: T): IPersistentStorage<T> { + const globalKeysStorage = new PersistentState<KeysStorage[]>(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<T>(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<void> { + 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<rpc.MessageWriter> { + // windows implementation of FIFO using named pipes + if (isWindows()) { + const deferred = createDeferred<rpc.MessageWriter>(); + 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<Error>(); + + private _onClose = new rpc.Emitter<void>(); + + private _onPartialMessage = new rpc.Emitter<rpc.PartialMessageInfo>(); + + // 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<Error> = this._onError.event; + + onClose: rpc.Event<void> = this._onClose.event; + + onPartialMessage: rpc.Event<rpc.PartialMessageInfo> = 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<rpc.MessageReader> { + if (isWindows()) { + // windows implementation of FIFO using named pipes + const deferred = createDeferred<rpc.MessageReader>(); + 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 a1c33dd0a605..000000000000 --- a/src/client/common/platform/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// TO DO: Deprecate in favor of IPlatformService -export const WINDOWS_PATH_VARIABLE_NAME = 'Path'; -export const NON_WINDOWS_PATH_VARIABLE_NAME = 'PATH'; -export const IS_WINDOWS = /^win/.test(process.platform); diff --git a/src/client/common/platform/errors.ts b/src/client/common/platform/errors.ts new file mode 100644 index 000000000000..9533f1bca50c --- /dev/null +++ b/src/client/common/platform/errors.ts @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as vscode from 'vscode'; + +/* +See: + + https://nodejs.org/api/errors.html + + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + + node_modules/@types/node/globals.d.ts + */ + +interface IError { + name: string; + message: string; + + toString(): string; +} + +interface INodeJSError extends IError { + code: string; + stack?: string; + stackTraceLimit: number; + + captureStackTrace(): void; +} + +//================================ +// "system" errors + +namespace vscErrors { + const FILE_NOT_FOUND = vscode.FileSystemError.FileNotFound().name; + const FILE_EXISTS = vscode.FileSystemError.FileExists().name; + const IS_DIR = vscode.FileSystemError.FileIsADirectory().name; + const NOT_DIR = vscode.FileSystemError.FileNotADirectory().name; + const NO_PERM = vscode.FileSystemError.NoPermissions().name; + const known = [ + // (order does not matter) + FILE_NOT_FOUND, + FILE_EXISTS, + IS_DIR, + NOT_DIR, + NO_PERM, + ]; + function errorMatches(err: Error, expectedName: string): boolean | undefined { + if (!known.includes(err.name)) { + return undefined; + } + return err.name === expectedName; + } + + export function isFileNotFound(err: Error): boolean | undefined { + return errorMatches(err, FILE_NOT_FOUND); + } + export function isFileExists(err: Error): boolean | undefined { + return errorMatches(err, FILE_EXISTS); + } + export function isFileIsDir(err: Error): boolean | undefined { + return errorMatches(err, IS_DIR); + } + export function isNotDir(err: Error): boolean | undefined { + return errorMatches(err, NOT_DIR); + } + export function isNoPermissions(err: Error): boolean | undefined { + return errorMatches(err, NO_PERM); + } +} + +interface ISystemError extends INodeJSError { + errno: number; + syscall: string; + info?: string; + path?: string; + address?: string; + dest?: string; + port?: string; +} + +// Return a new error for errno ENOTEMPTY. +export function createDirNotEmptyError(dirname: string): ISystemError { + const err = new Error(`directory "${dirname}" not empty`) as ISystemError; + err.name = 'SystemError'; + err.code = 'ENOTEMPTY'; + err.path = dirname; + err.syscall = 'rmdir'; + return err; +} + +function isSystemError(err: Error, expectedCode: string): boolean | undefined { + const code = (err as ISystemError).code; + if (!code) { + return undefined; + } + return code === expectedCode; +} + +// Return true if the given error is ENOENT. +export function isFileNotFoundError(err: unknown | Error): boolean | undefined { + const error = err as Error; + const matched = vscErrors.isFileNotFound(error); + if (matched !== undefined) { + return matched; + } + return isSystemError(error, 'ENOENT'); +} + +// Return true if the given error is EEXIST. +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(error, 'EEXIST'); +} + +// Return true if the given error is EISDIR. +export function isFileIsDirError(err: Error): boolean | undefined { + const matched = vscErrors.isFileIsDir(err); + if (matched !== undefined) { + return matched; + } + return isSystemError(err, 'EISDIR'); +} + +// Return true if the given error is ENOTDIR. +export function isNotDirError(err: Error): boolean | undefined { + const matched = vscErrors.isNotDir(err); + if (matched !== undefined) { + return matched; + } + return isSystemError(err, 'ENOTDIR'); +} + +// Return true if the given error is EACCES. +export function isNoPermissionsError(err: unknown | Error): boolean | undefined { + const error = err as Error; + const matched = vscErrors.isNoPermissions(error); + if (matched !== undefined) { + return matched; + } + return isSystemError(error, 'EACCES'); +} diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts index b30950d9cfbf..3e7f441654ec 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -1,195 +1,605 @@ +/* eslint-disable max-classes-per-file */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; import { createHash } from 'crypto'; -import * as fileSystem from 'fs'; import * as fs from 'fs-extra'; import * as glob from 'glob'; -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import * as tmp from 'tmp'; -import { traceDecorators } from '../logger'; -import { createDeferred } from '../utils/async'; -import { IFileSystem, IPlatformService, TemporaryFile } from './types'; +import { injectable } from 'inversify'; +import { promisify } from 'util'; +import * as vscode from 'vscode'; +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'; +import { + FileStat, + FileType, + IFileSystem, + IFileSystemPaths, + IFileSystemPathUtils, + IFileSystemUtils, + IRawFileSystem, + ITempFileSystem, + ReadStream, + TemporaryFile, + WriteStream, +} from './types'; -@injectable() -export class FileSystem implements IFileSystem { - constructor(@inject(IPlatformService) private platformService: IPlatformService) { } +const ENCODING = 'utf8'; - public get directorySeparatorChar(): string { - return path.sep; +export function convertStat(old: fs.Stats, filetype: FileType): FileStat { + return { + type: filetype, + size: old.size, + // FileStat.ctime and FileStat.mtime only have 1-millisecond + // resolution, while node provides nanosecond resolution. So + // for now we round to the nearest integer. + // See: https://github.com/microsoft/vscode/issues/84526 + ctime: Math.round(old.ctimeMs), + mtime: Math.round(old.mtimeMs), + }; +} + +function filterByFileType( + files: [string, FileType][], // the files to filter + fileType: FileType, // the file type to look for +): [string, FileType][] { + // We preserve the pre-existing behavior of following symlinks. + if (fileType === FileType.Unknown) { + // FileType.Unknown == 0 so we can't just use bitwise + // operations blindly here. + return files.filter( + ([_file, ft]) => ft === FileType.Unknown || ft === (FileType.SymbolicLink & FileType.Unknown), + ); } + return files.filter(([_file, ft]) => (ft & fileType) > 0); +} - public objectExists(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise<boolean> { - return new Promise<boolean>(resolve => { - fs.stat(filePath, (error, stats) => { - if (error) { - return resolve(false); - } - return resolve(statCheck(stats)); - }); - }); +// "raw" filesystem + +// This is the parts of the vscode.workspace.fs API that we use here. +// See: https://code.visualstudio.com/api/references/vscode-api#FileSystem +// Note that we have used all the API functions *except* "rename()". +interface IVSCodeFileSystemAPI { + copy(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable<void>; + createDirectory(uri: vscode.Uri): Thenable<void>; + delete(uri: vscode.Uri, options?: { recursive: boolean; useTrash: boolean }): Thenable<void>; + readDirectory(uri: vscode.Uri): Thenable<[string, FileType][]>; + readFile(uri: vscode.Uri): Thenable<Uint8Array>; + rename(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable<void>; + stat(uri: vscode.Uri): Thenable<FileStat>; + writeFile(uri: vscode.Uri, content: Uint8Array): Thenable<void>; +} + +// This is the parts of the 'fs-extra' module that we use in RawFileSystem. +interface IRawFSExtra { + lstat(filename: string): Promise<fs.Stats>; + chmod(filePath: string, mode: string | number): Promise<void>; + appendFile(filename: string, data: unknown): Promise<void>; + + // non-async + lstatSync(filename: string): fs.Stats; + statSync(filename: string): fs.Stats; + readFileSync(path: string, encoding: string): string; + createReadStream(filename: string): ReadStream; + createWriteStream(filename: string): WriteStream; + pathExists(filename: string): Promise<boolean>; +} + +interface IRawPath { + dirname(path: string): string; + join(...paths: string[]): string; +} + +// Later we will drop "FileSystem", switching usage to +// "FileSystemUtils" and then rename "RawFileSystem" to "FileSystem". + +// The low-level filesystem operations used by the extension. +export class RawFileSystem implements IRawFileSystem { + constructor( + // the low-level FS path operations to use + protected readonly paths: IRawPath, + // the VS Code FS API to use + protected readonly vscfs: IVSCodeFileSystemAPI, + // the node FS API to use + protected readonly fsExtra: IRawFSExtra, + ) {} + + // Create a new object using common-case default values. + public static withDefaults( + paths?: IRawPath, // default: a new FileSystemPaths object (using defaults) + vscfs?: IVSCodeFileSystemAPI, // default: the actual "vscode.workspace.fs" namespace + fsExtra?: IRawFSExtra, // default: the "fs-extra" module + ): RawFileSystem { + return new RawFileSystem( + paths || FileSystemPaths.withDefaults(), + vscfs || vscode.workspace.fs, + // The "fs-extra" module is effectively equivalent to node's "fs" + // module (but is a bit more async-friendly). So we use that + // instead of "fs". + fsExtra || (fs as IRawFSExtra), + ); } - public fileExists(filePath: string): Promise<boolean> { - return this.objectExists(filePath, (stats) => stats.isFile()); + public async pathExists(filename: string): Promise<boolean> { + return this.fsExtra.pathExists(filename); } - public fileExistsSync(filePath: string): boolean { - return fs.existsSync(filePath); - } - /** - * Reads the contents of the file using utf8 and returns the string contents. - * @param {string} filePath - * @returns {Promise<string>} - * @memberof FileSystem - */ - public readFile(filePath: string): Promise<string> { - return fs.readFile(filePath).then(buffer => buffer.toString()); - } - - public async writeFile(filePath: string, data: {}, options: string | fs.WriteFileOptions = { encoding: 'utf8' }): Promise<void> { - await fs.writeFile(filePath, data, options); - } - - public directoryExists(filePath: string): Promise<boolean> { - return this.objectExists(filePath, (stats) => stats.isDirectory()); - } - - public createDirectory(directoryPath: string): Promise<void> { - return fs.mkdirp(directoryPath); - } - - public deleteDirectory(directoryPath: string): Promise<void> { - const deferred = createDeferred<void>(); - fs.rmdir(directoryPath, err => err ? deferred.reject(err) : deferred.resolve()); - return deferred.promise; - } - - public getSubDirectories(rootDir: string): Promise<string[]> { - return new Promise<string[]>(resolve => { - fs.readdir(rootDir, (error, files) => { - if (error) { - return resolve([]); - } - const subDirs: string[] = []; - files.forEach(name => { - const fullPath = path.join(rootDir, name); - try { - if (fs.statSync(fullPath).isDirectory()) { - subDirs.push(fullPath); - } - // tslint:disable-next-line:no-empty - } catch (ex) { } - }); - resolve(subDirs); - }); - }); + + public async stat(filename: string): Promise<FileStat> { + // Note that, prior to the November release of VS Code, + // stat.ctime was always 0. + // See: https://github.com/microsoft/vscode/issues/84525 + const uri = vscode.Uri.file(filename); + return this.vscfs.stat(uri); } - public async getFiles(rootDir: string): Promise<string[]> { - const files = await fs.readdir(rootDir); - return files.filter(async f => { - const fullPath = path.join(rootDir, f); - if ((await fs.stat(fullPath)).isFile()) { - return true; - } - return false; - }); + public async lstat(filename: string): Promise<FileStat> { + // TODO https://github.com/microsoft/vscode/issues/71204 (84514)): + // This functionality has been requested for the VS Code API. + const stat = await this.fsExtra.lstat(filename); + // Note that, unlike stat(), lstat() does not include the type + // of the symlink's target. + const fileType = convertFileType(stat); + return convertStat(stat, fileType); } - public arePathsSame(path1: string, path2: string): boolean { - path1 = path.normalize(path1); - path2 = path.normalize(path2); - if (this.platformService.isWindows) { - return path1.toUpperCase() === path2.toUpperCase(); - } else { - return path1 === path2; + public async chmod(filename: string, mode: string | number): Promise<void> { + // TODO (https://github.com/microsoft/vscode/issues/73122 (84513)): + // This functionality has been requested for the VS Code API. + return this.fsExtra.chmod(filename, mode); + } + + public async move(src: string, tgt: string): Promise<void> { + const srcUri = vscode.Uri.file(src); + const tgtUri = vscode.Uri.file(tgt); + // The VS Code API will automatically create the target parent + // directory if it does not exist (even though the docs imply + // otherwise). So we have to manually stat, just to be sure. + // Note that this behavior was reported, but won't be changing. + // See: https://github.com/microsoft/vscode/issues/84177 + await this.vscfs.stat(vscode.Uri.file(this.paths.dirname(tgt))); + // We stick with the pre-existing behavior where files are + // overwritten and directories are not. + const options = { overwrite: false }; + try { + await this.vscfs.rename(srcUri, tgtUri, options); + } catch (err) { + if (!isFileExistsError(err)) { + throw err; // re-throw + } + const stat = await this.vscfs.stat(tgtUri); + if (stat.type === FileType.Directory) { + throw err; // re-throw + } + options.overwrite = true; + await this.vscfs.rename(srcUri, tgtUri, options); } } - public appendFileSync(filename: string, data: {}, encoding: string): void; - public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string }): void; - // tslint:disable-next-line:unified-signatures - public appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string }): void; - public appendFileSync(filename: string, data: {}, optionsOrEncoding: {}): void { - return fs.appendFileSync(filename, data, optionsOrEncoding); + public async readData(filename: string): Promise<Buffer> { + const uri = vscode.Uri.file(filename); + const data = await this.vscfs.readFile(uri); + return Buffer.from(data); } - public getRealPath(filePath: string): Promise<string> { - return new Promise<string>(resolve => { - fs.realpath(filePath, (err, realPath) => { - resolve(err ? filePath : realPath); - }); - }); + public async readText(filename: string): Promise<string> { + const uri = vscode.Uri.file(filename); + const result = await this.vscfs.readFile(uri); + const data = Buffer.from(result); + return data.toString(ENCODING); } - public copyFile(src: string, dest: string): Promise<void> { - const deferred = createDeferred<void>(); - const rs = fs.createReadStream(src).on('error', (err) => { - deferred.reject(err); - }); - const ws = fs.createWriteStream(dest).on('error', (err) => { - deferred.reject(err); - }).on('close', () => { - deferred.resolve(); + public async writeText(filename: string, text: string): Promise<void> { + const uri = vscode.Uri.file(filename); + const data = Buffer.from(text); + await this.vscfs.writeFile(uri, data); + } + + public async appendText(filename: string, text: string): Promise<void> { + // TODO: We *could* use the new API for this. + // See https://github.com/microsoft/vscode-python/issues/9900 + return this.fsExtra.appendFile(filename, text); + } + + public async copyFile(src: string, dest: string): Promise<void> { + const srcURI = vscode.Uri.file(src); + const destURI = vscode.Uri.file(dest); + // The VS Code API will automatically create the target parent + // directory if it does not exist (even though the docs imply + // otherwise). So we have to manually stat, just to be sure. + // Note that this behavior was reported, but won't be changing. + // See: https://github.com/microsoft/vscode/issues/84177 + await this.vscfs.stat(vscode.Uri.file(this.paths.dirname(dest))); + await this.vscfs.copy(srcURI, destURI, { + overwrite: true, }); - rs.pipe(ws); - return deferred.promise; - } - - public deleteFile(filename: string): Promise<void> { - const deferred = createDeferred<void>(); - fs.unlink(filename, err => err ? deferred.reject(err) : deferred.resolve()); - return deferred.promise; - } - - @traceDecorators.error('Failed to get FileHash') - public getFileHash(filePath: string): Promise<string | undefined> { - return new Promise<string | undefined>(resolve => { - fs.lstat(filePath, (err, stats) => { - if (err) { - resolve(); - } else { - const actual = createHash('sha512').update(`${stats.ctimeMs}-${stats.mtimeMs}`).digest('hex'); - resolve(actual); - } - }); + } + + public async rmfile(filename: string): Promise<void> { + const uri = vscode.Uri.file(filename); + return this.vscfs.delete(uri, { + recursive: false, + useTrash: false, }); } - public search(globPattern: string): Promise<string[]> { - return new Promise<string[]>((resolve, reject) => { - glob(globPattern, (ex, files) => { - if (ex) { - return reject(ex); - } - resolve(Array.isArray(files) ? files : []); - }); + + public async rmdir(dirname: string): Promise<void> { + const uri = vscode.Uri.file(dirname); + // The "recursive" option disallows directories, even if they + // are empty. So we have to deal with this ourselves. + const files = await this.vscfs.readDirectory(uri); + if (files && files.length > 0) { + throw createDirNotEmptyError(dirname); + } + return this.vscfs.delete(uri, { + recursive: true, + useTrash: false, }); } - public createTemporaryFile(extension: string): Promise<TemporaryFile> { - return new Promise<TemporaryFile>((resolve, reject) => { - tmp.file({ postfix: extension }, (err, tmpFile, _, cleanupCallback) => { - if (err) { - return reject(err); - } - resolve({ filePath: tmpFile, dispose: cleanupCallback }); - }); + + public async rmtree(dirname: string): Promise<void> { + const uri = vscode.Uri.file(dirname); + // TODO (https://github.com/microsoft/vscode/issues/84177): + // The docs say "throws - FileNotFound when uri doesn't exist". + // However, it happily does nothing. So for now we have to + // manually stat, just to be sure. + await this.vscfs.stat(uri); + return this.vscfs.delete(uri, { + recursive: true, + useTrash: false, }); } - public createWriteStream(filePath: string): fileSystem.WriteStream { - return fileSystem.createWriteStream(filePath); + public async mkdirp(dirname: string): Promise<void> { + const uri = vscode.Uri.file(dirname); + await this.vscfs.createDirectory(uri); } - public chmod(filePath: string, mode: string): Promise<void> { - return new Promise<void>((resolve, reject) => { - fileSystem.chmod(filePath, mode, (err: NodeJS.ErrnoException) => { - if (err) { - return reject(err); - } - resolve(); - }); + public async listdir(dirname: string): Promise<[string, FileType][]> { + const uri = vscode.Uri.file(dirname); + const files = await this.vscfs.readDirectory(uri); + return files.map(([basename, filetype]) => { + const filename = this.paths.join(dirname, basename); + return [filename, filetype] as [string, FileType]; }); } + + // non-async + + // VS Code has decided to never support any sync functions (aside + // from perhaps create*Stream()). + // See: https://github.com/microsoft/vscode/issues/84518 + + public statSync(filename: string): FileStat { + // We follow the filetype behavior of the VS Code API, by + // acknowledging symlinks. + let stat = this.fsExtra.lstatSync(filename); + let filetype = FileType.Unknown; + if (stat.isSymbolicLink()) { + filetype = FileType.SymbolicLink; + stat = this.fsExtra.statSync(filename); + } + filetype |= convertFileType(stat); + return convertStat(stat, filetype); + } + + public readTextSync(filename: string): string { + return this.fsExtra.readFileSync(filename, ENCODING); + } + + public createReadStream(filename: string): ReadStream { + // TODO (https://github.com/microsoft/vscode/issues/84515): + // This functionality has been requested for the VS Code API. + return this.fsExtra.createReadStream(filename); + } + + public createWriteStream(filename: string): WriteStream { + // TODO (https://github.com/microsoft/vscode/issues/84515): + // This functionality has been requested for the VS Code API. + return this.fsExtra.createWriteStream(filename); + } +} + +// filesystem "utils" + +// High-level filesystem operations used by the extension. +export class FileSystemUtils implements IFileSystemUtils { + constructor( + public readonly raw: IRawFileSystem, + public readonly pathUtils: IFileSystemPathUtils, + public readonly paths: IFileSystemPaths, + public readonly tmp: ITempFileSystem, + private readonly getHash: (data: string) => string, + private readonly globFiles: (pat: string, options?: { cwd: string; dot?: boolean }) => Promise<string[]>, + ) {} + + // 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<string[]>, + ): FileSystemUtils { + pathUtils = pathUtils || FileSystemPathUtils.withDefaults(); + return new FileSystemUtils( + raw || RawFileSystem.withDefaults(pathUtils.paths), + pathUtils, + pathUtils.paths, + tmp || TemporaryFileSystem.withDefaults(), + getHash || getHashString, + globFiles || promisify(glob.default), + ); + } + + // aliases + + public async createDirectory(directoryPath: string): Promise<void> { + return this.raw.mkdirp(directoryPath); + } + + public async deleteDirectory(directoryPath: string): Promise<void> { + return this.raw.rmdir(directoryPath); + } + + public async deleteFile(filename: string): Promise<void> { + return this.raw.rmfile(filename); + } + + // helpers + + public async pathExists( + // the "file" to look for + filename: string, + // the file type to expect; if not provided then any file type + // matches; otherwise a mismatch results in a "false" value + fileType?: FileType, + ): Promise<boolean> { + 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 + // means that any symlinks are getting resolved. + stat = await this.raw.stat(filename); + } catch (err) { + if (isFileNotFoundError(err)) { + return false; + } + traceError(`stat() failed for "${filename}"`, err); + return false; + } + + if (fileType === 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<boolean> { + return this.pathExists(filename, FileType.File); + } + + public async directoryExists(dirname: string): Promise<boolean> { + return this.pathExists(dirname, FileType.Directory); + } + + public async listdir(dirname: string): Promise<[string, FileType][]> { + try { + return await this.raw.listdir(dirname); + } catch (err) { + // We're only preserving pre-existng behavior here... + if (!(await this.pathExists(dirname))) { + return []; + } + throw err; // re-throw + } + } + + public async getSubDirectories(dirname: string): Promise<string[]> { + const files = await this.listdir(dirname); + const filtered = filterByFileType(files, FileType.Directory); + return filtered.map(([filename, _fileType]) => filename); + } + + public async getFiles(dirname: string): Promise<string[]> { + // Note that only "regular" files are returned. + const files = await this.listdir(dirname); + const filtered = filterByFileType(files, FileType.File); + return filtered.map(([filename, _fileType]) => filename); + } + + public async isDirReadonly(dirname: string): Promise<boolean> { + const filePath = `${dirname}${this.paths.sep}___vscpTest___`; + try { + await this.raw.stat(dirname); + await this.raw.writeText(filePath, ''); + } catch (err) { + if (isNoPermissionsError(err)) { + return true; + } + throw err; // re-throw + } + this.raw + .rmfile(filePath) + // Clean resources in the background. + .ignoreErrors(); + return false; + } + + public async getFileHash(filename: string): Promise<string> { + // The reason for lstat rather than stat is not clear... + const stat = await this.raw.lstat(filename); + const data = `${stat.ctime}-${stat.mtime}`; + return this.getHash(data); + } + + public async search(globPattern: string, cwd?: string, dot?: boolean): Promise<string[]> { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let options: any; + if (cwd) { + options = { ...options, cwd }; + } + if (dot) { + options = { ...options, dot }; + } + + const found = await this.globFiles(globPattern, options); + return Array.isArray(found) ? found : []; + } + + // helpers (non-async) + + public fileExistsSync(filePath: string): boolean { + try { + this.raw.statSync(filePath); + } catch (err) { + if (isFileNotFoundError(err)) { + return false; + } + throw err; // re-throw + } + return true; + } +} + +export function getHashString(data: string): string { + const hash = createHash('sha512'); + hash.update(data); + return hash.digest('hex'); +} + +// legacy filesystem API + +// more aliases (to cause less churn) +@injectable() +export class FileSystem implements IFileSystem { + // We expose this for the sake of functional tests that do not have + // access to the actual "vscode" namespace. + protected utils: FileSystemUtils; + + constructor() { + this.utils = FileSystemUtils.withDefaults(); + } + + public get directorySeparatorChar(): string { + return this.utils.paths.sep; + } + + public arePathsSame(path1: string, path2: string): boolean { + return this.utils.pathUtils.arePathsSame(path1, path2); + } + + public getDisplayName(path: string): string { + return this.utils.pathUtils.getDisplayName(path); + } + + public async stat(filename: string): Promise<FileStat> { + return this.utils.raw.stat(filename); + } + + public async createDirectory(dirname: string): Promise<void> { + return this.utils.createDirectory(dirname); + } + + public async deleteDirectory(dirname: string): Promise<void> { + return this.utils.deleteDirectory(dirname); + } + + public async listdir(dirname: string): Promise<[string, FileType][]> { + return this.utils.listdir(dirname); + } + + public async readFile(filePath: string): Promise<string> { + return this.utils.raw.readText(filePath); + } + + public async readData(filePath: string): Promise<Buffer> { + return this.utils.raw.readData(filePath); + } + + // eslint-disable-next-line @typescript-eslint/ban-types + public async writeFile(filename: string, data: string | Buffer): Promise<void> { + return this.utils.raw.writeText(filename, data); + } + + public async appendFile(filename: string, text: string): Promise<void> { + return this.utils.raw.appendText(filename, text); + } + + public async copyFile(src: string, dest: string): Promise<void> { + return this.utils.raw.copyFile(src, dest); + } + + public async deleteFile(filename: string): Promise<void> { + return this.utils.deleteFile(filename); + } + + public async chmod(filename: string, mode: string): Promise<void> { + return this.utils.raw.chmod(filename, mode); + } + + public async move(src: string, tgt: string): Promise<void> { + 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<boolean> { + return this.utils.fileExists(filename); + } + + public pathExists(filename: string): Promise<boolean> { + return this.utils.pathExists(filename); + } + + public fileExistsSync(filename: string): boolean { + return this.utils.fileExistsSync(filename); + } + + public async directoryExists(dirname: string): Promise<boolean> { + return this.utils.directoryExists(dirname); + } + + public async getSubDirectories(dirname: string): Promise<string[]> { + return this.utils.getSubDirectories(dirname); + } + + public async getFiles(dirname: string): Promise<string[]> { + return this.utils.getFiles(dirname); + } + + public async getFileHash(filename: string): Promise<string> { + return this.utils.getFileHash(filename); + } + + public async search(globPattern: string, cwd?: string, dot?: boolean): Promise<string[]> { + return this.utils.search(globPattern, cwd, dot); + } + + public async createTemporaryFile(suffix: string, mode?: number): Promise<TemporaryFile> { + return this.utils.tmp.createFile(suffix, mode); + } + + public async isDirReadonly(dirname: string): Promise<boolean> { + 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 new file mode 100644 index 000000000000..fa809d31b0b9 --- /dev/null +++ b/src/client/common/platform/fs-paths.ts @@ -0,0 +1,370 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// 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'; + +// The parts of node's 'path' module used by FileSystemPaths. +interface INodePath { + sep: string; + join(...filenames: string[]): string; + dirname(filename: string): string; + basename(filename: string, ext?: string): string; + normalize(filename: string): string; +} + +export class FileSystemPaths implements IFileSystemPaths { + constructor( + // "true" if targeting a case-insensitive host (like Windows) + private readonly isCaseInsensitive: boolean, + // (effectively) the node "path" module to use + private readonly raw: INodePath, + ) {} + // Create a new object using common-case default values. + // We do not use an alternate constructor because defaults in the + // constructor runs counter to our typical approach. + public static withDefaults( + // default: use "isWindows" + isCaseInsensitive?: boolean, + ): FileSystemPaths { + if (isCaseInsensitive === undefined) { + isCaseInsensitive = getOSType() === OSType.Windows; + } + return new FileSystemPaths( + isCaseInsensitive, + // Use the actual node "path" module. + nodepath, + ); + } + + public get sep(): string { + return this.raw.sep; + } + + public join(...filenames: string[]): string { + return this.raw.join(...filenames); + } + + public dirname(filename: string): string { + return this.raw.dirname(filename); + } + + public basename(filename: string, suffix?: string): string { + return this.raw.basename(filename, suffix); + } + + public normalize(filename: string): string { + return this.raw.normalize(filename); + } + + public normCase(filename: string): string { + filename = this.raw.normalize(filename); + return this.isCaseInsensitive ? filename.toUpperCase() : filename; + } +} + +export class Executables { + constructor( + // the $PATH delimiter to use + public readonly delimiter: string, + // the OS type to target + private readonly osType: OSType, + ) {} + // Create a new object using common-case default values. + // We do not use an alternate constructor because defaults in the + // constructor runs counter to our typical approach. + public static withDefaults(): Executables { + return new Executables( + // Use node's value. + nodepath.delimiter, + // Use the current OS. + getOSType(), + ); + } + + public get envVar(): string { + return getSearchPathEnvVarNames(this.osType)[0]; + } +} + +// The dependencies FileSystemPathUtils has on node's path module. +interface IRawPaths { + relative(relpath: string, rootpath: string): string; +} + +export class FileSystemPathUtils implements IFileSystemPathUtils { + constructor( + // the user home directory to use (and expose) + public readonly home: string, + // the low-level FS path operations to use (and expose) + public readonly paths: IFileSystemPaths, + // the low-level OS "executables" to use (and expose) + public readonly executables: IExecutables, + // other low-level FS path operations to use + private readonly raw: IRawPaths, + ) {} + // Create a new object using common-case default values. + // We do not use an alternate constructor because defaults in the + // constructor runs counter to our typical approach. + public static withDefaults( + // default: a new FileSystemPaths object (using defaults) + paths?: IFileSystemPaths, + ): FileSystemPathUtils { + if (paths === undefined) { + paths = FileSystemPaths.withDefaults(); + } + return new FileSystemPathUtils( + // Use the current user's home directory. + os.homedir(), + paths, + Executables.withDefaults(), + // Use the actual node "path" module. + nodepath, + ); + } + + public arePathsSame(path1: string, path2: string): boolean { + path1 = this.paths.normCase(path1); + path2 = this.paths.normCase(path2); + return path1 === path2; + } + + public getDisplayName(filename: string, cwd?: string): string { + if (cwd && isParentPath(filename, cwd)) { + return `.${this.paths.sep}${this.raw.relative(cwd, filename)}`; + } 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<void> { + 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<string> { + return fs.readlink(path); +} + +export function unlink(path: string): Promise<void> { + return fs.unlink(path); +} + +export function symlink(target: string, path: string, type?: fs.SymlinkType): Promise<void> { + 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<fs.Stats> { + return fs.stat(path); +} + +export function lstat(path: string): Promise<fs.Stats> { + return fs.lstat(path); +} + +export function chmod(path: string, mod: fs.Mode): Promise<void> { + 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<boolean> { + return fs.pathExists(absPath); +} + +export function createFile(filename: string): Promise<void> { + return fs.createFile(filename); +} + +export function rmdir(path: string, options?: fs.RmDirOptions): Promise<void> { + return fs.rmdir(path, options); +} + +export function remove(path: string): Promise<void> { + return fs.remove(path); +} + +export function readFile(filePath: string, encoding: BufferEncoding): Promise<string>; +export function readFile(filePath: string): Promise<Buffer>; +export function readFile(filePath: string, options: { encoding: BufferEncoding }): Promise<string>; +export function readFile( + filePath: string, + options?: { encoding: BufferEncoding } | BufferEncoding | undefined, +): Promise<string | Buffer> { + if (typeof options === 'string') { + return fs.readFile(filePath, { encoding: options }); + } + return fs.readFile(filePath, options); +} + +export function readJson(filePath: string): Promise<any> { + return fs.readJson(filePath); +} + +export function writeFile(filePath: string, data: any, options?: { encoding: BufferEncoding }): Promise<void> { + return fs.writeFile(filePath, data, options); +} + +export function mkdir(dirPath: string): Promise<void> { + return fs.mkdir(dirPath); +} + +export function mkdirp(dirPath: string): Promise<void> { + return fs.mkdirp(dirPath); +} + +export function rename(oldPath: string, newPath: string): Promise<void> { + return fs.rename(oldPath, newPath); +} + +export function ensureDir(dirPath: string): Promise<void> { + return fs.ensureDir(dirPath); +} + +export function ensureFile(filePath: string): Promise<void> { + return fs.ensureFile(filePath); +} + +export function ensureSymlink(target: string, filePath: string, type?: fs.SymlinkType): Promise<void> { + return fs.ensureSymlink(target, filePath, type); +} + +export function appendFile(filePath: string, data: any, options?: { encoding: BufferEncoding }): Promise<void> { + return fs.appendFile(filePath, data, options); +} + +export function readdir(path: string): Promise<string[]>; +export function readdir( + path: string, + options: fs.ObjectEncodingOptions & { + withFileTypes: true; + }, +): Promise<fs.Dirent[]>; +export function readdir( + path: fs.PathLike, + options?: fs.ObjectEncodingOptions & { + withFileTypes: true; + }, +): Promise<string[] | fs.Dirent[]> { + if (options === undefined) { + return fs.readdir(path); + } + return fs.readdir(path, options); +} + +export function emptyDir(dirPath: string): Promise<void> { + return fs.emptyDir(dirPath); +} diff --git a/src/client/common/platform/fs-temp.ts b/src/client/common/platform/fs-temp.ts new file mode 100644 index 000000000000..60dde040f454 --- /dev/null +++ b/src/client/common/platform/fs-temp.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as tmp from 'tmp'; +import { ITempFileSystem, TemporaryFile } from './types'; + +interface IRawTempFS { + 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, + ) {} + public static withDefaults(): TemporaryFileSystem { + return new TemporaryFileSystem( + // Use the actual "tmp" module. + tmp, + ); + } + + // Create a new temp file with the given filename suffix. + public createFile(suffix: string, mode?: number): Promise<TemporaryFile> { + const opts = { + postfix: suffix, + mode, + }; + return new Promise<TemporaryFile>((resolve, reject) => { + 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 87b2b5c22354..b3be39f4644b 100644 --- a/src/client/common/platform/pathUtils.ts +++ b/src/client/common/platform/pathUtils.ts @@ -1,36 +1,53 @@ +// TODO: Drop this file. +// See https://github.com/microsoft/vscode-python/issues/8542. + import { inject, injectable } from 'inversify'; import * as path from 'path'; import { IPathUtils, IsWindows } from '../types'; -import { NON_WINDOWS_PATH_VARIABLE_NAME, WINDOWS_PATH_VARIABLE_NAME } from './constants'; -// tslint:disable-next-line:no-var-requires no-require-imports -const untildify = require('untildify'); +import { OSType } from '../utils/platform'; +import { Executables, FileSystemPaths, FileSystemPathUtils } from './fs-paths'; +import { untildify } from '../helpers'; @injectable() export class PathUtils implements IPathUtils { - public readonly home = ''; - constructor(@inject(IsWindows) private isWindows: boolean) { - this.home = untildify('~'); + private readonly utils: FileSystemPathUtils; + constructor( + // "true" if targeting a Windows host. + @inject(IsWindows) isWindows: boolean, + ) { + const osType = isWindows ? OSType.Windows : OSType.Unknown; + // We cannot just use FileSystemPathUtils.withDefaults() because + // of the isWindows arg. + this.utils = new FileSystemPathUtils( + untildify('~'), + FileSystemPaths.withDefaults(), + new Executables(path.delimiter, osType), + path, + ); + } + + public get home(): string { + return this.utils.home; } + public get delimiter(): string { - return path.delimiter; + return this.utils.executables.delimiter; } + public get separator(): string { - return path.sep; - } - // TO DO: Deprecate in favor of IPlatformService - public getPathVariableName() { - return this.isWindows ? WINDOWS_PATH_VARIABLE_NAME : NON_WINDOWS_PATH_VARIABLE_NAME; + return this.utils.paths.sep; } - public basename(pathValue: string, ext?: string): string { - return path.basename(pathValue, ext); + + // TODO: Deprecate in favor of IPlatformService? + public getPathVariableName(): 'Path' | 'PATH' { + return this.utils.executables.envVar as any; } + public getDisplayName(pathValue: string, cwd?: string): string { - if (cwd && pathValue.startsWith(cwd)) { - return `.${path.sep}${path.relative(cwd, pathValue)}`; - } else if (pathValue.startsWith(this.home)) { - return `~${path.sep}${path.relative(this.home, pathValue)}`; - } else { - return pathValue; - } + return this.utils.getDisplayName(pathValue, cwd); + } + + public basename(pathValue: string, ext?: string): string { + return this.utils.paths.basename(pathValue, ext); } } diff --git a/src/client/common/platform/platformService.ts b/src/client/common/platform/platformService.ts index 42a42bab8468..dc9b04cc652c 100644 --- a/src/client/common/platform/platformService.ts +++ b/src/client/common/platform/platformService.ts @@ -1,29 +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 { PLATFORM_INFO, PlatformErrors } from '../../telemetry/constants'; -import { traceDecorators, traceError } from '../logger'; -import { 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; - 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'; } - @traceDecorators.verbose('Get Platform Version') + public async getVersion(): Promise<SemVer> { if (this.version) { return this.version; @@ -37,45 +38,38 @@ export class PlatformService implements IPlatformService { try { const ver = coerce(os.release()); if (ver) { - sendTelemetryEvent(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(PLATFORM_INFO, undefined, { failureType: PlatformErrors.FailedToParseVersion }); - traceError(`Failed to parse Version ${os.release()}`, ex); - 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; } - public get is64bit(): boolean { - // tslint:disable-next-line:no-require-imports - const arch = require('arch') as typeof import('arch'); - return arch() === 'x64'; + + // eslint-disable-next-line class-methods-use-this + public get osRelease(): string { + return os.release(); } -} -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 { - sendTelemetryEvent(PLATFORM_INFO, undefined, { failureType: PlatformErrors.FailedToDetermineOS }); - return OSType.Unknown; + // eslint-disable-next-line class-methods-use-this + public get is64bit(): boolean { + return getArchitecture() === Architecture.x64; } } diff --git a/src/client/common/platform/registry.ts b/src/client/common/platform/registry.ts index 31f758f62ad8..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,9 +39,8 @@ 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<string | undefined | null>((resolve, reject) => { + return new Promise<string | undefined | null>((resolve) => { new Registry(options).get(name, (error, result) => { if (error || !result || typeof result.value !== 'string') { return resolve(undefined); @@ -43,15 +51,14 @@ async function getRegistryValue(options: Options, name: string = '') { } async function getRegistryKeys(options: Options): Promise<string[]> { - // 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<string[]>((resolve, reject) => { + return new Promise<string[]>((resolve) => { new Registry(options).keys((error, result) => { if (error || !Array.isArray(result)) { return resolve([]); } - resolve(result.filter(item => typeof item.key === 'string').map(item => item.key)); + resolve(result.filter((item) => typeof item.key === 'string').map((item) => item.key)); }); }); } @@ -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 1214a77e9fa7..11edc9ada0aa 100644 --- a/src/client/common/platform/types.ts +++ b/src/client/common/platform/types.ts @@ -4,11 +4,21 @@ import * as fs from 'fs'; import * as fsextra from 'fs-extra'; import { SemVer } from 'semver'; -import { Disposable } from 'vscode'; +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 + HKCU, + HKLM, } export const IRegistry = Symbol('IRegistry'); @@ -17,9 +27,13 @@ export interface IRegistry { getValue(key: string, hive: RegistryHive, arch?: Architecture, name?: string): Promise<string | undefined | null>; } +//= ========================== +// platform + export const IPlatformService = Symbol('IPlatformService'); export interface IPlatformService { readonly osType: OSType; + osRelease: string; readonly pathVariableName: 'Path' | 'PATH'; readonly virtualEnvBinName: 'bin' | 'Scripts'; @@ -31,33 +45,186 @@ export interface IPlatformService { getVersion(): Promise<SemVer>; } -export type TemporaryFile = { filePath: string } & Disposable; -export type TemporaryDirectory = { path: string } & Disposable; +//= ========================== +// temp FS + +export type TemporaryFile = { filePath: string } & vscode.Disposable; + +export interface ITempFileSystem { + createFile(suffix: string, mode?: number): Promise<TemporaryFile>; +} + +//= ========================== +// FS paths + +// The low-level file path operations used by the extension. +export interface IFileSystemPaths { + readonly sep: string; + join(...filenames: string[]): string; + dirname(filename: string): string; + basename(filename: string, suffix?: string): string; + normalize(filename: string): string; + normCase(filename: string): string; +} + +// Where to fine executables. +// +// In particular this class provides all the tools needed to find +// executables, including through an environment variable. +export interface IExecutables { + delimiter: string; + envVar: string; +} + +export const IFileSystemPathUtils = Symbol('IFileSystemPathUtils'); +// A collection of high-level utilities related to filesystem paths. +export interface IFileSystemPathUtils { + readonly paths: IFileSystemPaths; + readonly executables: IExecutables; + readonly home: string; + // Return true if the two paths are equivalent on the current + // filesystem and false otherwise. On Windows this is significant. + // On non-Windows the filenames must always be exactly the same. + arePathsSame(path1: string, path2: string): boolean; + // Return the clean (displayable) form of the given filename. + getDisplayName(pathValue: string, cwd?: string): string; +} + +//= ========================== +// filesystem operations + +// The low-level filesystem operations on which the extension depends. +export interface IRawFileSystem { + pathExists(filename: string): Promise<boolean>; + // Get information about a file (resolve symlinks). + stat(filename: string): Promise<FileStat>; + // Get information about a file (do not resolve synlinks). + lstat(filename: string): Promise<FileStat>; + // Change a file's permissions. + chmod(filename: string, mode: string | number): Promise<void>; + // Move the file to a different location (and/or rename it). + move(src: string, tgt: string): Promise<void>; + + //* ********************** + // files + + // Return the raw bytes of the given file. + readData(filename: string): Promise<Buffer>; + // Return the text of the given file (decoded from UTF-8). + readText(filename: string): Promise<string>; + // Write the given text to the file (UTF-8 encoded). + writeText(filename: string, data: string | Buffer): Promise<void>; + // Write the given text to the end of the file (UTF-8 encoded). + appendText(filename: string, text: string): Promise<void>; + // Copy a file. + copyFile(src: string, dest: string): Promise<void>; + // Delete a file. + rmfile(filename: string): Promise<void>; + + //* ********************** + // directories + + // Create the directory and any missing parent directories. + mkdirp(dirname: string): Promise<void>; + // Delete the directory if empty. + rmdir(dirname: string): Promise<void>; + // Delete the directory and everything in it. + rmtree(dirname: string): Promise<void>; + // Return the contents of the directory. + listdir(dirname: string): Promise<[string, FileType][]>; + + //* ********************** + // not async + + // Get information about a file (resolve symlinks). + statSync(filename: string): FileStat; + // Return the text of the given file (decoded from UTF-8). + readTextSync(filename: string): string; + // Create a streaming wrappr around an open file (for reading). + createReadStream(filename: string): ReadStream; + // Create a streaming wrappr around an open file (for writing). + createWriteStream(filename: string): WriteStream; +} + +// High-level filesystem operations used by the extension. +export interface IFileSystemUtils { + readonly raw: IRawFileSystem; + readonly paths: IFileSystemPaths; + readonly pathUtils: IFileSystemPathUtils; + readonly tmp: ITempFileSystem; + + //* ********************** + // aliases + + createDirectory(dirname: string): Promise<void>; + deleteDirectory(dirname: string): Promise<void>; + deleteFile(filename: string): Promise<void>; + + //* ********************** + // helpers + + // Determine if the file exists, optionally requiring the type. + pathExists(filename: string, fileType?: FileType): Promise<boolean>; + // Determine if the regular file exists. + fileExists(filename: string): Promise<boolean>; + // Determine if the directory exists. + directoryExists(dirname: string): Promise<boolean>; + // Get all the directory's entries. + listdir(dirname: string): Promise<[string, FileType][]>; + // Get the paths of all immediate subdirectories. + getSubDirectories(dirname: string): Promise<string[]>; + // Get the paths of all immediately contained files. + getFiles(dirname: string): Promise<string[]>; + // Determine if the directory is read-only. + isDirReadonly(dirname: string): Promise<boolean>; + // Generate the sha512 hash for the file (based on timestamps). + getFileHash(filename: string): Promise<string>; + // Get the paths of all files matching the pattern. + search(globPattern: string): Promise<string[]>; + + //* ********************** + // helpers (non-async) + + fileExistsSync(path: string): boolean; +} + +// TODO: Later we will drop IFileSystem, switching usage to IFileSystemUtils. +// See https://github.com/microsoft/vscode-python/issues/8542. export const IFileSystem = Symbol('IFileSystem'); export interface IFileSystem { + // path-related directorySeparatorChar: string; - objectExists(path: string, statCheck: (s: fs.Stats) => boolean): Promise<boolean>; - fileExists(path: string): Promise<boolean>; - fileExistsSync(path: string): boolean; - directoryExists(path: string): Promise<boolean>; + arePathsSame(path1: string, path2: string): boolean; + getDisplayName(path: string): string; + + // "raw" operations + stat(filePath: string): Promise<FileStat>; createDirectory(path: string): Promise<void>; deleteDirectory(path: string): Promise<void>; - getSubDirectories(rootDir: string): Promise<string[]>; - getFiles(rootDir: string): Promise<string[]>; - arePathsSame(path1: string, path2: string): boolean; + listdir(dirname: string): Promise<[string, FileType][]>; readFile(filePath: string): Promise<string>; - writeFile(filePath: string, data: {}, options?: string | fsextra.WriteFileOptions): Promise<void>; - appendFileSync(filename: string, data: {}, encoding: string): void; - appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string }): void; - // tslint:disable-next-line:unified-signatures - appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: string; flag?: string }): void; - getRealPath(path: string): Promise<string>; + readData(filePath: string): Promise<Buffer>; + writeFile(filePath: string, text: string | Buffer, options?: string | fsextra.WriteFileOptions): Promise<void>; + appendFile(filename: string, text: string | Buffer): Promise<void>; copyFile(src: string, dest: string): Promise<void>; deleteFile(filename: string): Promise<void>; - getFileHash(filePath: string): Promise<string | undefined>; - search(globPattern: string): Promise<string[]>; - createTemporaryFile(extension: string): Promise<TemporaryFile>; + chmod(path: string, mode: string | number): Promise<void>; + move(src: string, tgt: string): Promise<void>; + // sync + readFileSync(filename: string): string; + createReadStream(path: string): fs.ReadStream; createWriteStream(path: string): fs.WriteStream; - chmod(path: string, mode: string): Promise<void>; + + // utils + pathExists(path: string): Promise<boolean>; + fileExists(path: string): Promise<boolean>; + fileExistsSync(path: string): boolean; + directoryExists(path: string): Promise<boolean>; + getSubDirectories(rootDir: string): Promise<string[]>; + getFiles(rootDir: string): Promise<string[]>; + getFileHash(filePath: string): Promise<string>; + search(globPattern: string, cwd?: string, dot?: boolean): Promise<string[]>; + createTemporaryFile(extension: string, mode?: number): Promise<TemporaryFile>; + isDirReadonly(dirname: string): Promise<boolean>; } diff --git a/src/client/common/process/currentProcess.ts b/src/client/common/process/currentProcess.ts index 14e8355afe45..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'; @@ -13,9 +11,9 @@ export class CurrentProcess implements ICurrentProcess { public on = (event: string | symbol, listener: Function): this => { process.on(event as any, listener as any); return process as any; - } + }; public get env(): EnvironmentVariables { - return process.env as any as EnvironmentVariables; + return (process.env as any) as EnvironmentVariables; } public get argv(): string[] { return process.argv; 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 new file mode 100644 index 000000000000..377c6580bfd5 --- /dev/null +++ b/src/client/common/process/internal/python.ts @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// "python" contains functions corresponding to the various ways that +// the extension invokes a Python executable internally. Each function +// takes arguments relevant to the specific use case. However, each +// always *returns* a list of strings for the commandline arguments that +// should be used when invoking the Python executable for the specific +// use case, whether through spawn/exec or a terminal. +// +// Where relevant (nearly always), the function also returns a "parse" +// function that may be used to deserialize the stdout of the command +// into the corresponding object or objects. "parse()" takes a single +// string as the stdout text and returns the relevant data. + +export function execCode(code: string): 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[]): string[] { + const args = ['-m', name, ...moduleArgs]; + // "code" isn't specific enough to know how to parse it, + // so we only return the args. + return args; +} + +export function getExecutable(): [string[], (out: string) => string] { + const args = ['-c', 'import sys;print(sys.executable)']; + + function parse(out: string): string { + return out.trim(); + } + + return [args, parse]; +} + +export function getSitePackages(): [string[], (out: string) => string] { + // 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(); + } + + return [args, parse]; +} + +export function getUserSitePackages(): [string[], (out: string) => string] { + const args = ['site', '--user-site']; + + function parse(out: string): string { + return out.trim(); + } + + return [args, parse]; +} + +export function isValid(): [string[], (out: string) => boolean] { + const args = ['-c', 'print(1234)']; + + function parse(out: string): boolean { + return out.startsWith('1234'); + } + + return [args, parse]; +} + +export function isModuleInstalled(name: string): [string[], (out: string) => boolean] { + const args = ['-c', `import ${name}`]; + + function parse(_out: string): boolean { + // If the command did not fail then the module is installed. + return true; + } + + return [args, parse]; +} + +export function getModuleVersion(name: string): [string[], (out: string) => string] { + const args = ['-c', `import ${name}; print(${name}.__version__)`]; + + function parse(out: string): string { + return out.trim(); + } + + return [args, parse]; +} 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 new file mode 100644 index 000000000000..f2c905c02889 --- /dev/null +++ b/src/client/common/process/internal/scripts/index.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { _SCRIPTS_DIR } from './constants'; + +const SCRIPTS_DIR = _SCRIPTS_DIR; + +// "scripts" contains everything relevant to the scripts found under +// 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 +// or a terminal. +// +// Where relevant (nearly always), the function also returns a "parse" +// function that may be used to deserialize the stdout of the script +// into the corresponding object or objects. "parse()" takes a single +// string as the stdout text and returns the relevant data. +// +// Some of the scripts are located in subdirectories of "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()". +export * as testingTools from './testing_tools'; + +// interpreterInfo.py + +type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; +type PythonVersionInfo = [number, number, number, ReleaseLevel, number]; +export type InterpreterInfoJson = { + versionInfo: PythonVersionInfo; + sysPrefix: string; + sysVersion: string; + is64Bit: boolean; +}; + +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 = [script]; + + function parse(out: string): InterpreterInfoJson { + try { + return JSON.parse(out); + } catch (ex) { + throw Error(`python ${args} returned bad JSON (${out}) (${ex})`); + } + } + + return [args, parse]; +} + +// normalizeSelection.py + +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. + return out; + } + + return [args, parse]; +} + +// printEnvVariables.py + +export function printEnvVariables(): [string[], (out: string) => NodeJS.ProcessEnv] { + const script = path.join(SCRIPTS_DIR, 'printEnvVariables.py').fileToCommandArgumentForPythonExt(); + const args = [script]; + + 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 [ + script, + command.fileToCommandArgumentForPythonExt(), + // The shell args must come after the command + // but before the lockfile. + ...shellArgs, + 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 [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 new file mode 100644 index 000000000000..60dd21b698b6 --- /dev/null +++ b/src/client/common/process/internal/scripts/testing_tools.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { _SCRIPTS_DIR } from './constants'; + +const SCRIPTS_DIR = path.join(_SCRIPTS_DIR, 'testing_tools'); + +//============================ +// run_adapter.py + +export function runAdapter(adapterArgs: string[]): string[] { + const script = path.join(SCRIPTS_DIR, 'run_adapter.py'); + return [script, ...adapterArgs]; +} + +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/logger.ts b/src/client/common/process/logger.ts new file mode 100644 index 000000000000..b65da8dc81e5 --- /dev/null +++ b/src/client/common/process/logger.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { 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(IWorkspaceService) private readonly workspaceService: IWorkspaceService) {} + + 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; + } + 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) => { + 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 b1180cf9003b..4a5aa984fa44 100644 --- a/src/client/common/process/proc.ts +++ b/src/client/common/process/proc.ts @@ -1,27 +1,21 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { exec, spawn } from 'child_process'; -import { Observable } from 'rxjs/Observable'; -import * as tk from 'tree-kill'; -import { Disposable } from 'vscode'; +import { EventEmitter } from 'events'; +import { traceError } from '../../logging'; -import { createDeferred } from '../utils/async'; +import { IDisposable } from '../types'; 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'; + +export class ProcessService extends EventEmitter implements IProcessService { + private processesToKill = new Set<IDisposable>(); + + constructor(private readonly env?: EnvironmentVariables) { + super(); + } -// tslint:disable:no-any -export class ProcessService implements IProcessService { - constructor(private readonly decoder: IBufferDecoder, private readonly env?: EnvironmentVariables) { } public static isAlive(pid: number): boolean { try { process.kill(pid, 0); @@ -30,165 +24,55 @@ export class ProcessService implements IProcessService { return false; } } + public static kill(pid: number): void { - // tslint:disable-next-line:no-require-imports - const killProcessTree = require('tree-kill'); - try { - killProcessTree(pid); - } catch { - // Ignore. - } + killPid(pid); } - public execObservable(file: string, args: string[], options: SpawnOptions = {}): ObservableExecutionResult<string> { - const spawnOptions = this.getDefaultOptions(options); - const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; - const proc = spawn(file, args, spawnOptions); - let procExited = false; - - const output = new Observable<Output<string>>(subscriber => { - const disposables: Disposable[] = []; - - const on = (ee: NodeJS.EventEmitter, name: string, fn: Function) => { - ee.on(name, fn as any); - disposables.push({ dispose: () => ee.removeListener(name, fn as any) }); - }; - - if (options.token) { - disposables.push(options.token.onCancellationRequested(() => { - if (!procExited && !proc.killed) { - proc.kill(); - procExited = true; - } - })); + public dispose(): void { + this.removeAllListeners(); + this.processesToKill.forEach((p) => { + try { + p.dispose(); + } catch { + // ignore. } - - 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(disposable => disposable.dispose()); - }); - proc.once('error', ex => { - procExited = true; - subscriber.error(ex); - disposables.forEach(disposable => disposable.dispose()); - }); }); - - return { - proc, - out: output, - dispose: () => { - if (proc && !proc.killed) { - tk(proc.pid); - } - } - }; } - public exec(file: string, args: string[], options: SpawnOptions = {}): Promise<ExecutionResult<string>> { - const spawnOptions = this.getDefaultOptions(options); - const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; - const proc = spawn(file, args, spawnOptions); - const deferred = createDeferred<ExecutionResult<string>>(); - const disposables: Disposable[] = []; - const on = (ee: NodeJS.EventEmitter, name: string, fn: Function) => { - ee.on(name, fn as any); - disposables.push({ dispose: () => ee.removeListener(name, fn as any) }); - }; + public execObservable(file: string, args: string[], options: SpawnOptions = {}): ObservableExecutionResult<string> { + const execOptions = { ...options, doNotLog: true }; + const result = execObservable(file, args, execOptions, this.env, this.processesToKill); + this.emit('exec', file, args, options); + return result; + } - if (options.token) { - disposables.push(options.token.onCancellationRequested(() => { - if (!proc.killed && !deferred.completed) { - proc.kill(); - } - })); + public exec(file: string, args: string[], options: SpawnOptions = {}): Promise<ExecutionResult<string>> { + this.emit('exec', file, args, options); + if (options.useWorker) { + return workerPlainExec(file, args, options); } - - 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(disposable => disposable.dispose()); - }); - proc.once('error', ex => { - deferred.reject(ex); - disposables.forEach(disposable => disposable.dispose()); - }); - - return deferred.promise; + const execOptions = { ...options, doNotLog: true }; + const promise = plainExec(file, args, execOptions, this.env, this.processesToKill); + return promise; } public shellExec(command: string, options: ShellOptions = {}): Promise<ExecutionResult<string>> { - const shellOptions = this.getDefaultOptions(options); - return new Promise((resolve, reject) => { - 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<IDisposable>(); + 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}`); } }); }); } - - private getDefaultOptions<T extends (ShellOptions | SpawnOptions)>(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 }; - } - - // 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 91440cf9bddd..40204a640dae 100644 --- a/src/client/common/process/processFactory.ts +++ b/src/client/common/process/processFactory.ts @@ -5,20 +5,24 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; -import { IServiceContainer } from '../../ioc/types'; +import { IDisposableRegistry } from '../types'; import { IEnvironmentVariablesProvider } from '../variables/types'; import { ProcessService } from './proc'; -import { IBufferDecoder, IProcessService, IProcessServiceFactory } from './types'; +import { IProcessLogger, IProcessService, IProcessServiceFactory } from './types'; @injectable() export class ProcessServiceFactory implements IProcessServiceFactory { - private envVarsService: IEnvironmentVariablesProvider; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.envVarsService = serviceContainer.get<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider); - } - public async create(resource?: Uri): Promise<IProcessService> { - const customEnvVars = await this.envVarsService.getEnvironmentVariables(resource); - const decoder = this.serviceContainer.get<IBufferDecoder>(IBufferDecoder); - return new ProcessService(decoder, customEnvVars); + constructor( + @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider, + @inject(IProcessLogger) private readonly processLogger: IProcessLogger, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + ) {} + public async create(resource?: Uri, options?: { doNotUseCustomEnvs: boolean }): Promise<IProcessService> { + 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/pythonEnvironment.ts b/src/client/common/process/pythonEnvironment.ts new file mode 100644 index 000000000000..cbf898ac5f50 --- /dev/null +++ b/src/client/common/process/pythonEnvironment.ts @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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 { isTestExecution } from '../constants'; +import { IFileSystem } from '../platform/types'; +import * as internalPython from './internal/python'; +import { ExecutionResult, IProcessService, IPythonEnvironment, ShellOptions, SpawnOptions } from './types'; +import { PixiEnvironmentInfo } from '../../pythonEnvironments/common/environmentManagers/pixi'; + +const cachedExecutablePath: Map<string, Promise<string | undefined>> = new Map<string, Promise<string | undefined>>(); + +class PythonEnvironment implements IPythonEnvironment { + private cachedInterpreterInformation: InterpreterInformation | undefined | null = null; + + constructor( + protected readonly pythonPath: string, + // "deps" is the externally defined functionality used by the class. + protected readonly deps: { + getPythonArgv(python: string): string[]; + getObservablePythonArgv(python: string): string[]; + isValidExecutable(python: string): Promise<boolean>; + // from ProcessService: + exec(file: string, args: string[]): Promise<ExecutionResult<string>>; + shellExec(command: string, options?: ShellOptions): Promise<ExecutionResult<string>>; + }, + ) {} + + public getExecutionInfo(pythonArgs: string[] = [], pythonExecutable?: string): PythonExecInfo { + const python = this.deps.getPythonArgv(this.pythonPath); + return buildPythonExecInfo(python, pythonArgs, pythonExecutable); + } + public getExecutionObservableInfo(pythonArgs: string[] = [], pythonExecutable?: string): PythonExecInfo { + const python = this.deps.getObservablePythonArgv(this.pythonPath); + return buildPythonExecInfo(python, pythonArgs, pythonExecutable); + } + + public async getInterpreterInformation(): Promise<InterpreterInformation | undefined> { + if (this.cachedInterpreterInformation === null) { + this.cachedInterpreterInformation = await this.getInterpreterInformationImpl(); + } + return this.cachedInterpreterInformation; + } + + public async getExecutablePath(): Promise<string | undefined> { + // 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(); + const promise = getExecutablePath(python, this.deps.shellExec); + cachedExecutablePath.set(this.pythonPath, promise); + return promise; + } + + public async getModuleVersion(moduleName: string): Promise<string | undefined> { + const [args, parse] = internalPython.getModuleVersion(moduleName); + const info = this.getExecutionInfo(args); + let data: ExecutionResult<string>; + 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<boolean> { + // prettier-ignore + const [args,] = internalPython.isModuleInstalled(moduleName); + const info = this.getExecutionInfo(args); + try { + await this.deps.exec(info.command, info.args); + } catch (ex) { + traceVerbose(`Error when checking if module is installed ${moduleName}`, ex); + return false; + } + return true; + } + + private async getInterpreterInformationImpl(): Promise<InterpreterInformation | undefined> { + try { + const python = this.getExecutionInfo(); + return await getInterpreterInfo(python, this.deps.shellExec, { verbose: traceVerbose, error: traceError }); + } catch (ex) { + traceError(`Failed to get interpreter information for '${this.pythonPath}'`, ex); + } + } +} + +function createDeps( + isValidExecutable: (filename: string) => Promise<boolean>, + pythonArgv: string[] | undefined, + observablePythonArgv: string[] | undefined, + // from ProcessService: + exec: (file: string, args: string[], options?: SpawnOptions) => Promise<ExecutionResult<string>>, + shellExec: (command: string, options?: ShellOptions) => Promise<ExecutionResult<string>>, +) { + return { + 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, + }; +} + +export function createPythonEnv( + pythonPath: string, + // These are used to generate the deps. + procs: IProcessService, + fs: IFileSystem, +): PythonEnvironment { + const deps = createDeps( + 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), + ); + return new PythonEnvironment(pythonPath, deps); +} + +export async function createCondaEnv( + condaInfo: CondaEnvironmentInfo, + // These are used to generate the deps. + procs: IProcessService, + fs: IFileSystem, +): Promise<PythonEnvironment | undefined> { + const conda = await Conda.getConda(); + const pythonArgv = await conda?.getRunPythonArgs({ name: condaInfo.name, prefix: condaInfo.path }); + if (!pythonArgv) { + return undefined; + } + const deps = createDeps( + async (filename) => fs.pathExists(filename), + pythonArgv, + pythonArgv, + (file, args, opts) => procs.exec(file, args, opts), + (command, opts) => procs.shellExec(command, opts), + ); + const interpreterPath = await conda?.getInterpreterPathForEnvironment({ + name: condaInfo.name, + prefix: condaInfo.path, + }); + if (!interpreterPath) { + return undefined; + } + return new PythonEnvironment(interpreterPath, deps); +} + +export async function createPixiEnv( + pixiEnv: PixiEnvironmentInfo, + // These are used to generate the deps. + procs: IProcessService, + fs: IFileSystem, +): Promise<PythonEnvironment | undefined> { + 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, +): PythonEnvironment { + const deps = createDeps( + /** + * 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 microsoft store python + * interpreters. + */ + async (_f: string) => true, + // We use the default: [pythonPath]. + undefined, + undefined, + (file, args, opts) => procs.exec(file, args, opts), + (command, opts) => procs.shellExec(command, opts), + ); + return new PythonEnvironment(pythonPath, deps); +} diff --git a/src/client/common/process/pythonExecutionFactory.ts b/src/client/common/process/pythonExecutionFactory.ts index 3e9551905cec..efb05c3c9d12 100644 --- a/src/client/common/process/pythonExecutionFactory.ts +++ b/src/client/common/process/pythonExecutionFactory.ts @@ -1,24 +1,191 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; + +import { IEnvironmentActivationService } from '../../interpreter/activation/types'; +import { IActivatedEnvironmentLaunch, IComponentAdapter } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { IConfigurationService } from '../types'; -import { PythonExecutionService } from './pythonProcess'; -import { ExecutionFactoryCreationOptions, IProcessServiceFactory, IPythonExecutionFactory, IPythonExecutionService } from './types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IFileSystem } from '../platform/types'; +import { IConfigurationService, IDisposableRegistry, IInterpreterPathService } from '../types'; +import { ProcessService } from './proc'; +import { createCondaEnv, createPythonEnv, createMicrosoftStoreEnv, createPixiEnv } from './pythonEnvironment'; +import { createPythonProcessService } from './pythonProcess'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionFactoryCreationOptions, + IProcessLogger, + IProcessService, + IProcessServiceFactory, + IPythonEnvironment, + IPythonExecutionFactory, + IPythonExecutionService, +} from './types'; +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 configService: IConfigurationService; - private processServiceFactory: IProcessServiceFactory; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.processServiceFactory = serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory); - this.configService = serviceContainer.get<IConfigurationService>(IConfigurationService); + 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(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>(IDisposableRegistry); + this.logger = this.serviceContainer.get<IProcessLogger>(IProcessLogger); + this.fileSystem = this.serviceContainer.get<IFileSystem>(IFileSystem); } + public async create(options: ExecutionFactoryCreationOptions): Promise<IPythonExecutionService> { - const pythonPath = options.pythonPath ? options.pythonPath : this.configService.getSettings(options.resource).pythonPath; - const processService = await this.processServiceFactory.create(options.resource); - return new PythonExecutionService(this.serviceContainer, processService, pythonPath); + let { pythonPath } = options; + if (!pythonPath || pythonPath === 'python') { + const activatedEnvLaunch = this.serviceContainer.get<IActivatedEnvironmentLaunch>( + 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); + + 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 windowsStoreInterpreterCheck = this.pyenvs.isMicrosoftStoreInterpreter.bind(this.pyenvs); + + const env = (await windowsStoreInterpreterCheck(pythonPath)) + ? createMicrosoftStoreEnv(pythonPath, processService) + : createPythonEnv(pythonPath, processService, this.fileSystem); + + return createPythonService(processService, env); + } + + public async createActivatedEnvironment( + options: ExecutionFactoryCreateWithEnvironmentOptions, + ): Promise<IPythonExecutionService> { + const envVars = await this.activationHelper.getActivatedEnvironmentVariables( + options.resource, + options.interpreter, + options.allowEnvironmentFetchExceptions, + ); + const hasEnvVars = envVars && Object.keys(envVars).length > 0; + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, undefined, { hasEnvVars }); + if (!hasEnvVars) { + return this.create({ + resource: options.resource, + pythonPath: options.interpreter ? options.interpreter.path : undefined, + }); + } + const pythonPath = options.interpreter + ? options.interpreter.path + : this.configService.getSettings(options.resource).pythonPath; + const processService: IProcessService = new ProcessService({ ...envVars }); + processService.on('exec', this.logger.logProcess.bind(this.logger)); + this.disposables.push(processService); + + 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); + } + + public async createCondaExecutionService( + pythonPath: string, + processService: IProcessService, + ): Promise<IPythonExecutionService | undefined> { + const condaLocatorService = this.serviceContainer.get<IComponentAdapter>(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); } + + public async createPixiExecutionService( + pythonPath: string, + processService: IProcessService, + ): Promise<IPythonExecutionService | undefined> { + 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(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), + 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 6ba03ccd2e49..f4d1de8883ba 100644 --- a/src/client/common/process/pythonProcess.ts +++ b/src/client/common/process/pythonProcess.ts @@ -1,93 +1,104 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { injectable } from 'inversify'; -import * as path from 'path'; -import { IServiceContainer } from '../../ioc/types'; -import { EXTENSION_ROOT_DIR } from '../constants'; +import { PythonExecInfo } from '../../pythonEnvironments/exec'; import { ErrorUtils } from '../errors/errorUtils'; import { ModuleNotInstalledError } from '../errors/moduleNotInstalledError'; -import { traceError } from '../logger'; -import { IFileSystem } from '../platform/types'; -import { Architecture } from '../utils/platform'; -import { parsePythonVersion } from '../utils/version'; -import { ExecutionResult, InterpreterInfomation, IProcessService, IPythonExecutionService, ObservableExecutionResult, PythonVersionInfo, SpawnOptions } from './types'; - -@injectable() -export class PythonExecutionService implements IPythonExecutionService { - private readonly fileSystem: IFileSystem; +import * as internalPython from './internal/python'; +import { ExecutionResult, IProcessService, IPythonEnvironment, ObservableExecutionResult, SpawnOptions } from './types'; +class PythonProcessService { constructor( - serviceContainer: IServiceContainer, - private readonly procService: IProcessService, - private readonly pythonPath: string - ) { - this.fileSystem = serviceContainer.get<IFileSystem>(IFileSystem); - } - - public async getInterpreterInformation(): Promise<InterpreterInfomation | undefined> { - const file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'interpreterInfo.py'); - try { - const jsonValue = await this.procService.exec(this.pythonPath, [file], { mergeStdOutErr: true }) - .then(output => output.stdout.trim()); - - let json: { versionInfo: PythonVersionInfo; sysPrefix: string; sysVersion: string; is64Bit: boolean }; - try { - json = JSON.parse(jsonValue); - } catch (ex) { - traceError(`Failed to parse interpreter information for '${this.pythonPath}' with JSON ${jsonValue}`, ex); - return; - } - const versionValue = json.versionInfo.length === 4 ? `${json.versionInfo.slice(0, 3).join('.')}-${json.versionInfo[3]}` : json.versionInfo.join('.'); - return { - architecture: json.is64Bit ? Architecture.x64 : Architecture.x86, - path: this.pythonPath, - version: parsePythonVersion(versionValue), - sysVersion: json.sysVersion, - sysPrefix: json.sysPrefix - }; - } catch (ex) { - traceError(`Failed to get interpreter information for '${this.pythonPath}'`, ex); - } - } - public async getExecutablePath(): Promise<string> { - // 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.fileSystem.fileExists(this.pythonPath)) { - return this.pythonPath; - } - return this.procService.exec(this.pythonPath, ['-c', 'import sys;print(sys.executable)'], { throwOnStdErr: true }) - .then(output => output.stdout.trim()); - } - public async isModuleInstalled(moduleName: string): Promise<boolean> { - return this.procService.exec(this.pythonPath, ['-c', `import ${moduleName}`], { throwOnStdErr: true }) - .then(() => true).catch(() => false); - } + // This is the externally defined functionality used by the class. + private readonly deps: { + // from PythonEnvironment: + isModuleInstalled(moduleName: string): Promise<boolean>; + getExecutionInfo(pythonArgs?: string[]): PythonExecInfo; + getExecutionObservableInfo(pythonArgs?: string[]): PythonExecInfo; + // from ProcessService: + exec(file: string, args: string[], options: SpawnOptions): Promise<ExecutionResult<string>>; + execObservable(file: string, args: string[], options: SpawnOptions): ObservableExecutionResult<string>; + }, + ) {} public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult<string> { const opts: SpawnOptions = { ...options }; - return this.procService.execObservable(this.pythonPath, args, opts); + const executable = this.deps.getExecutionObservableInfo(args); + return this.deps.execObservable(executable.command, executable.args, opts); } - public execModuleObservable(moduleName: string, args: string[], options: SpawnOptions): ObservableExecutionResult<string> { + + public execModuleObservable( + moduleName: string, + moduleArgs: string[], + options: SpawnOptions, + ): ObservableExecutionResult<string> { + const args = internalPython.execModule(moduleName, moduleArgs); const opts: SpawnOptions = { ...options }; - return this.procService.execObservable(this.pythonPath, ['-m', moduleName, ...args], opts); + const executable = this.deps.getExecutionObservableInfo(args); + return this.deps.execObservable(executable.command, executable.args, opts); } + public async exec(args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> { const opts: SpawnOptions = { ...options }; - return this.procService.exec(this.pythonPath, args, opts); + const executable = this.deps.getExecutionInfo(args); + return this.deps.exec(executable.command, executable.args, opts); } - public async execModule(moduleName: string, args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> { + + public async execModule( + moduleName: string, + moduleArgs: string[], + options: SpawnOptions, + ): Promise<ExecutionResult<string>> { + const args = internalPython.execModule(moduleName, moduleArgs); const opts: SpawnOptions = { ...options }; - const result = await this.procService.exec(this.pythonPath, ['-m', moduleName, ...args], opts); + 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.isModuleInstalled(moduleName!); + if (moduleName && ErrorUtils.outputHasModuleNotInstalledError(moduleName, result.stderr)) { + const isInstalled = await this.deps.isModuleInstalled(moduleName); if (!isInstalled) { - throw new ModuleNotInstalledError(moduleName!); + throw new ModuleNotInstalledError(moduleName); } } return result; } + + public async execForLinter( + moduleName: string, + args: string[], + options: SpawnOptions, + ): Promise<ExecutionResult<string>> { + 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: IPythonEnvironment, +) { + const deps = { + // from PythonService: + isModuleInstalled: async (m: string) => env.isModuleInstalled(m), + getExecutionInfo: (a?: string[]) => env.getExecutionInfo(a), + getExecutionObservableInfo: (a?: string[]) => env.getExecutionObservableInfo(a), + // from ProcessService: + exec: async (f: string, a: string[], o: SpawnOptions) => procs.exec(f, a, o), + execObservable: (f: string, a: string[], o: SpawnOptions) => procs.execObservable(f, a, o), + }; + return new PythonProcessService(deps); } diff --git a/src/client/common/process/pythonToolService.ts b/src/client/common/process/pythonToolService.ts index d4b2ccaaa8bb..136ab56fe0c4 100644 --- a/src/client/common/process/pythonToolService.ts +++ b/src/client/common/process/pythonToolService.ts @@ -5,33 +5,75 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; import { IServiceContainer } from '../../ioc/types'; import { ExecutionInfo } from '../types'; -import { ExecutionResult, IProcessServiceFactory, IPythonExecutionFactory, IPythonToolExecutionService, ObservableExecutionResult, SpawnOptions } from './types'; +import { + ExecutionResult, + IProcessServiceFactory, + IPythonExecutionFactory, + IPythonToolExecutionService, + ObservableExecutionResult, + SpawnOptions, +} from './types'; @injectable() export class PythonToolExecutionService implements IPythonToolExecutionService { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } - public async execObservable(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise<ObservableExecutionResult<string>> { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + public async execObservable( + executionInfo: ExecutionInfo, + options: SpawnOptions, + resource: Uri, + ): Promise<ObservableExecutionResult<string>> { if (options.env) { throw new Error('Environment variables are not supported'); } if (executionInfo.moduleName && executionInfo.moduleName.length > 0) { - const pythonExecutionService = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource }); + const pythonExecutionService = await this.serviceContainer + .get<IPythonExecutionFactory>(IPythonExecutionFactory) + .create({ resource }); return pythonExecutionService.execModuleObservable(executionInfo.moduleName, executionInfo.args, options); } else { - const processService = await this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(resource); + const processService = await this.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create(resource); return processService.execObservable(executionInfo.execPath!, executionInfo.args, { ...options }); } } - public async exec(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise<ExecutionResult<string>> { + public async exec( + executionInfo: ExecutionInfo, + options: SpawnOptions, + resource: Uri, + ): Promise<ExecutionResult<string>> { if (options.env) { throw new Error('Environment variables are not supported'); } if (executionInfo.moduleName && executionInfo.moduleName.length > 0) { - const pythonExecutionService = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource }); - return pythonExecutionService.execModule(executionInfo.moduleName!, executionInfo.args, options); + const pythonExecutionService = await this.serviceContainer + .get<IPythonExecutionFactory>(IPythonExecutionFactory) + .create({ resource }); + return pythonExecutionService.execModule(executionInfo.moduleName, executionInfo.args, options); } else { - const processService = await this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(resource); + const processService = await this.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create(resource); return processService.exec(executionInfo.execPath!, executionInfo.args, { ...options }); } } + + public async execForLinter( + executionInfo: ExecutionInfo, + options: SpawnOptions, + resource: Uri, + ): Promise<ExecutionResult<string>> { + if (options.env) { + throw new Error('Environment variables are not supported'); + } + const pythonExecutionService = await this.serviceContainer + .get<IPythonExecutionFactory>(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<T extends ShellOptions | SpawnOptions>(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<IDisposable>, +): Promise<ExecutionResult<string>> { + 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<IDisposable>, +): Promise<ExecutionResult<string>> { + 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<ExecutionResult<string>>(); + 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]*)<<<PYTHON-EXEC-OUTPUT/; + const match = stdout.match(regex); + const filteredOut = match !== null && match.length >= 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('<<<PYTHON-EXEC-OUTPUT\r\n', '').replace('<<<PYTHON-EXEC-OUTPUT\n', ''); +} + +export function execObservable( + file: string, + args: string[], + options: SpawnOptions & { doNotLog?: boolean } = {}, + defaultEnv?: EnvironmentVariables, + disposables?: Set<IDisposable>, +): ObservableExecutionResult<string> { + 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<Output<string>>((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>(IBufferDecoder, BufferDecoder); serviceManager.addSingleton<IProcessServiceFactory>(IProcessServiceFactory, ProcessServiceFactory); serviceManager.addSingleton<IPythonExecutionFactory>(IPythonExecutionFactory, PythonExecutionFactory); serviceManager.addSingleton<IPythonToolExecutionService>(IPythonToolExecutionService, PythonToolExecutionService); diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index 84d42cd0e375..9263e69cbe21 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -1,16 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + import { ChildProcess, ExecOptions, SpawnOptions as ChildProcessSpawnOptions } from 'child_process'; import { Observable } from 'rxjs/Observable'; -import { CancellationToken, Uri } from 'vscode'; -import { ExecutionInfo, Version } from '../types'; -import { Architecture } from '../utils/platform'; -import { EnvironmentVariables } from '../variables/types'; - -export const IBufferDecoder = Symbol('IBufferDecoder'); -export interface IBufferDecoder { - decode(buffers: Buffer[], encoding: string): string; -} +import { CancellationToken, OutputChannel, Uri } from 'vscode'; +import { PythonExecInfo } from '../../pythonEnvironments/exec'; +import { InterpreterInformation, PythonEnvironment } from '../../pythonEnvironments/info'; +import { ExecutionInfo, IDisposable } from '../types'; export type Output<T extends string | Buffer> = { source: 'stdout' | 'stderr'; @@ -22,32 +18,45 @@ export type ObservableExecutionResult<T extends string | Buffer> = { 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<T extends string | Buffer> = { stdout: T; stderr?: T; }; -export interface IProcessService { +export const IProcessLogger = Symbol('IProcessLogger'); +export interface IProcessLogger { + /** + * 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 { execObservable(file: string, args: string[], options?: SpawnOptions): ObservableExecutionResult<string>; exec(file: string, args: string[], options?: SpawnOptions): Promise<ExecutionResult<string>>; shellExec(command: string, options?: ShellOptions): Promise<ExecutionResult<string>>; + on(event: 'exec', listener: (file: string, args: string[], options?: SpawnOptions) => void): this; } export const IProcessServiceFactory = Symbol('IProcessServiceFactory'); export interface IProcessServiceFactory { - create(resource?: Uri): Promise<IProcessService>; + create(resource?: Uri, options?: { doNotUseCustomEnvs: boolean }): Promise<IProcessService>; } export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory'); @@ -55,45 +64,67 @@ export type ExecutionFactoryCreationOptions = { resource?: Uri; pythonPath?: string; }; +export type ExecutionFactoryCreateWithEnvironmentOptions = { + resource?: Uri; + interpreter?: PythonEnvironment; + allowEnvironmentFetchExceptions?: boolean; + /** + * Ignore running `conda run` when running code. + * It is known to fail in certain scenarios. Where necessary we might want to bypass this. + * + * @type {boolean} + */ +}; export interface IPythonExecutionFactory { create(options: ExecutionFactoryCreationOptions): Promise<IPythonExecutionService>; + createActivatedEnvironment(options: ExecutionFactoryCreateWithEnvironmentOptions): Promise<IPythonExecutionService>; + createCondaExecutionService( + pythonPath: string, + processService: IProcessService, + ): Promise<IPythonExecutionService | undefined>; } -export type ReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final' | 'unknown'; -export type PythonVersionInfo = [number, number, number, ReleaseLevel]; -export type InterpreterInfomation = { - path: string; - version?: Version; - sysVersion: string; - architecture: Architecture; - sysPrefix: string; -}; export const IPythonExecutionService = Symbol('IPythonExecutionService'); export interface IPythonExecutionService { - getInterpreterInformation(): Promise<InterpreterInfomation | undefined>; - getExecutablePath(): Promise<string>; + getInterpreterInformation(): Promise<InterpreterInformation | undefined>; + getExecutablePath(): Promise<string | undefined>; isModuleInstalled(moduleName: string): Promise<boolean>; + getModuleVersion(moduleName: string): Promise<string | undefined>; + getExecutionInfo(pythonArgs?: string[]): PythonExecInfo; execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult<string>; execModuleObservable(moduleName: string, args: string[], options: SpawnOptions): ObservableExecutionResult<string>; exec(args: string[], options: SpawnOptions): Promise<ExecutionResult<string>>; execModule(moduleName: string, args: string[], options: SpawnOptions): Promise<ExecutionResult<string>>; + execForLinter(moduleName: string, args: string[], options: SpawnOptions): Promise<ExecutionResult<string>>; } +export interface IPythonEnvironment { + getInterpreterInformation(): Promise<InterpreterInformation | undefined>; + getExecutionObservableInfo(pythonArgs?: string[], pythonExecutable?: string): PythonExecInfo; + getExecutablePath(): Promise<string | undefined>; + isModuleInstalled(moduleName: string): Promise<boolean>; + getModuleVersion(moduleName: string): Promise<string | undefined>; + getExecutionInfo(pythonArgs?: string[], pythonExecutable?: string): PythonExecInfo; +} + +export type ShellExecFunc = (command: string, options?: ShellOptions | undefined) => Promise<ExecutionResult<string>>; + export class StdErrError extends Error { constructor(message: string) { super(message); } } -export interface IExecutionEnvironmentVariablesService { - getEnvironmentVariables(resource?: Uri): Promise<EnvironmentVariables | undefined>; -} - export const IPythonToolExecutionService = Symbol('IPythonToolRunnerService'); export interface IPythonToolExecutionService { - execObservable(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise<ObservableExecutionResult<string>>; + execObservable( + executionInfo: ExecutionInfo, + options: SpawnOptions, + resource: Uri, + ): Promise<ObservableExecutionResult<string>>; exec(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise<ExecutionResult<string>>; + execForLinter(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise<ExecutionResult<string>>; } 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<any> { + 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<ExecutionResult<string>> { + return executeWorkerFile(path.join(__dirname, 'shellExec.worker.js'), { + command, + options, + }); +} + +export function workerPlainExec( + file: string, + args: string[], + options: SpawnOptions = {}, +): Promise<ExecutionResult<string>> { + 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<void>; +} +export type EnvironmentVariables = Record<string, string | undefined>; +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<T extends string | Buffer> = { + 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<T extends ShellOptions | SpawnOptions>(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<IDisposable>, +): Promise<ExecutionResult<string>> { + 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<IDisposable>, +): Promise<ExecutionResult<string>> { + 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<ExecutionResult<string>>(); + 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]*)<<<PYTHON-EXEC-OUTPUT/; + const match = stdout.match(regex); + const filteredOut = match !== null && match.length >= 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/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index fea49c7b2c24..abd2b220e400 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -1,105 +1,191 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { IHttpClient } from '../activation/types'; +import { IExtensionSingleActivationService } from '../activation/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'; import { ApplicationEnvironment } from './application/applicationEnvironment'; import { ApplicationShell } from './application/applicationShell'; +import { ClipboardService } from './application/clipboard'; import { CommandManager } from './application/commandManager'; +import { ReloadVSCodeCommandHandler } from './application/commands/reloadCommand'; +import { ReportIssueCommandHandler } from './application/commands/reportIssueCommand'; import { DebugService } from './application/debugService'; import { DocumentManager } from './application/documentManager'; import { Extensions } from './application/extensions'; +import { LanguageService } from './application/languageService'; import { TerminalManager } from './application/terminalManager'; import { + IActiveResourceService, IApplicationEnvironment, IApplicationShell, + IClipboard, ICommandManager, + IContextKeyManager, IDebugService, IDocumentManager, + IJupyterExtensionDependencyManager, + ILanguageService, ITerminalManager, - IWorkspaceService + IWorkspaceService, } from './application/types'; import { WorkspaceService } from './application/workspace'; -import { AsyncDisposableRegistry } from './asyncDisposableRegistry'; import { ConfigurationService } from './configuration/service'; -import { EditorUtils } from './editor'; -import { FeatureDeprecationManager } from './featureDeprecationManager'; +import { PipEnvExecutionPath } from './configuration/executionSettings/pipEnvExecution'; +import { ExperimentService } from './experiments/service'; import { ProductInstaller } from './installer/productInstaller'; -import { Logger } from './logger'; +import { InterpreterPathService } from './interpreterPathService'; import { BrowserService } from './net/browser'; -import { HttpClient } from './net/httpClient'; -import { NugetService } from './nuget/nugetService'; -import { INugetService } from './nuget/types'; import { PersistentStateFactory } from './persistentState'; -import { IS_WINDOWS } from './platform/constants'; import { PathUtils } from './platform/pathUtils'; import { CurrentProcess } from './process/currentProcess'; +import { ProcessLogger } from './process/logger'; +import { IProcessLogger } from './process/types'; import { TerminalActivator } from './terminal/activator'; import { PowershellTerminalActivationFailedHandler } from './terminal/activator/powershellFailedHandler'; import { Bash } from './terminal/environmentActivationProviders/bash'; +import { Nushell } from './terminal/environmentActivationProviders/nushell'; import { CommandPromptAndPowerShell } from './terminal/environmentActivationProviders/commandPrompt'; +import { CondaActivationCommandProvider } from './terminal/environmentActivationProviders/condaActivationProvider'; +import { PipEnvActivationCommandProvider } from './terminal/environmentActivationProviders/pipEnvActivationProvider'; import { PyEnvActivationCommandProvider } from './terminal/environmentActivationProviders/pyenvActivationProvider'; import { TerminalServiceFactory } from './terminal/factory'; import { TerminalHelper } from './terminal/helper'; +import { SettingsShellDetector } from './terminal/shellDetectors/settingsShellDetector'; +import { TerminalNameShellDetector } from './terminal/shellDetectors/terminalNameShellDetector'; +import { UserEnvironmentShellDetector } from './terminal/shellDetectors/userEnvironmentShellDetector'; +import { VSCEnvironmentShellDetector } from './terminal/shellDetectors/vscEnvironmentShellDetector'; import { + IShellDetector, ITerminalActivationCommandProvider, ITerminalActivationHandler, ITerminalActivator, ITerminalHelper, - ITerminalServiceFactory + ITerminalServiceFactory, + TerminalActivationProviders, } from './terminal/types'; -import { - IAsyncDisposableRegistry, - IBrowserService, - IConfigurationService, - ICurrentProcess, - IEditorUtils, - IExtensions, - IFeatureDeprecationManager, - IInstaller, - ILogger, - 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'; -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingletonInstance<boolean>(IsWindows, IS_WINDOWS); +export function registerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingletonInstance<boolean>(IsWindows, isWindows()); + serviceManager.addSingleton<IActiveResourceService>(IActiveResourceService, ActiveResourceService); + serviceManager.addSingleton<IInterpreterPathService>(IInterpreterPathService, InterpreterPathService); serviceManager.addSingleton<IExtensions>(IExtensions, Extensions); serviceManager.addSingleton<IRandom>(IRandom, Random); serviceManager.addSingleton<IPersistentStateFactory>(IPersistentStateFactory, PersistentStateFactory); - serviceManager.addSingleton<ILogger>(ILogger, Logger); + serviceManager.addBinding(IPersistentStateFactory, IExtensionSingleActivationService); serviceManager.addSingleton<ITerminalServiceFactory>(ITerminalServiceFactory, TerminalServiceFactory); serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils); serviceManager.addSingleton<IApplicationShell>(IApplicationShell, ApplicationShell); + serviceManager.addSingleton<IClipboard>(IClipboard, ClipboardService); serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess); serviceManager.addSingleton<IInstaller>(IInstaller, ProductInstaller); + serviceManager.addSingleton<IJupyterExtensionDependencyManager>( + IJupyterExtensionDependencyManager, + JupyterExtensionDependencyManager, + ); + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + RequireJupyterPrompt, + ); + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + CreatePythonFileCommandHandler, + ); serviceManager.addSingleton<ICommandManager>(ICommandManager, CommandManager); + serviceManager.addSingleton<IContextKeyManager>(IContextKeyManager, ContextKeyManager); serviceManager.addSingleton<IConfigurationService>(IConfigurationService, ConfigurationService); serviceManager.addSingleton<IWorkspaceService>(IWorkspaceService, WorkspaceService); + serviceManager.addSingleton<IProcessLogger>(IProcessLogger, ProcessLogger); serviceManager.addSingleton<IDocumentManager>(IDocumentManager, DocumentManager); serviceManager.addSingleton<ITerminalManager>(ITerminalManager, TerminalManager); serviceManager.addSingleton<IDebugService>(IDebugService, DebugService); serviceManager.addSingleton<IApplicationEnvironment>(IApplicationEnvironment, ApplicationEnvironment); + serviceManager.addSingleton<ILanguageService>(ILanguageService, LanguageService); serviceManager.addSingleton<IBrowserService>(IBrowserService, BrowserService); - serviceManager.addSingleton<IHttpClient>(IHttpClient, HttpClient); - serviceManager.addSingleton<IEditorUtils>(IEditorUtils, EditorUtils); - serviceManager.addSingleton<INugetService>(INugetService, NugetService); serviceManager.addSingleton<ITerminalActivator>(ITerminalActivator, TerminalActivator); - serviceManager.addSingleton<ITerminalActivationHandler>(ITerminalActivationHandler, PowershellTerminalActivationFailedHandler); + serviceManager.addSingleton<ITerminalActivationHandler>( + ITerminalActivationHandler, + PowershellTerminalActivationFailedHandler, + ); + serviceManager.addSingleton<IExperimentService>(IExperimentService, ExperimentService); serviceManager.addSingleton<ITerminalHelper>(ITerminalHelper, TerminalHelper); serviceManager.addSingleton<ITerminalActivationCommandProvider>( - ITerminalActivationCommandProvider, Bash, 'bashCShellFish'); + ITerminalActivationCommandProvider, + Bash, + TerminalActivationProviders.bashCShellFish, + ); + serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + CommandPromptAndPowerShell, + TerminalActivationProviders.commandPromptAndPowerShell, + ); + serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + Nushell, + TerminalActivationProviders.nushell, + ); + serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + PyEnvActivationCommandProvider, + TerminalActivationProviders.pyenv, + ); + serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + CondaActivationCommandProvider, + TerminalActivationProviders.conda, + ); serviceManager.addSingleton<ITerminalActivationCommandProvider>( - ITerminalActivationCommandProvider, CommandPromptAndPowerShell, 'commandPromptAndPowerShell'); + ITerminalActivationCommandProvider, + PixiActivationCommandProvider, + TerminalActivationProviders.pixi, + ); serviceManager.addSingleton<ITerminalActivationCommandProvider>( - ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, 'pyenv'); - serviceManager.addSingleton<IFeatureDeprecationManager>(IFeatureDeprecationManager, FeatureDeprecationManager); + ITerminalActivationCommandProvider, + PipEnvActivationCommandProvider, + TerminalActivationProviders.pipenv, + ); + serviceManager.addSingleton<IToolExecutionPath>(IToolExecutionPath, PipEnvExecutionPath, ToolExecutionPath.pipenv); - serviceManager.addSingleton<IAsyncDisposableRegistry>(IAsyncDisposableRegistry, AsyncDisposableRegistry); serviceManager.addSingleton<IMultiStepInputFactory>(IMultiStepInputFactory, MultiStepInputFactory); + serviceManager.addSingleton<IImportTracker>(IImportTracker, ImportTracker); + serviceManager.addBinding(IImportTracker, IExtensionSingleActivationService); + serviceManager.addSingleton<IShellDetector>(IShellDetector, TerminalNameShellDetector); + serviceManager.addSingleton<IShellDetector>(IShellDetector, SettingsShellDetector); + serviceManager.addSingleton<IShellDetector>(IShellDetector, UserEnvironmentShellDetector); + serviceManager.addSingleton<IShellDetector>(IShellDetector, VSCEnvironmentShellDetector); + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + ReloadVSCodeCommandHandler, + ); + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + ReportIssueCommandHandler, + ); } 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 b74287a99f2a..b4d2f888d5d2 100644 --- a/src/client/common/terminal/activator/base.ts +++ b/src/client/common/terminal/activator/base.ts @@ -3,27 +3,35 @@ 'use strict'; -import { Terminal, Uri } from 'vscode'; +import { Terminal } from 'vscode'; +import { traceVerbose } from '../../../logging'; import { createDeferred, sleep } from '../../utils/async'; -import { ITerminalActivator, ITerminalHelper, TerminalShellType } from '../types'; +import { ITerminalActivator, ITerminalHelper, TerminalActivationOptions, TerminalShellType } from '../types'; export class BaseTerminalActivator implements ITerminalActivator { private readonly activatedTerminals: Map<Terminal, Promise<boolean>> = new Map<Terminal, Promise<boolean>>(); - constructor(private readonly helper: ITerminalHelper) { } - public async activateEnvironmentInTerminal(terminal: Terminal, resource: Uri | undefined, preserveFocus: boolean = true) { + constructor(private readonly helper: ITerminalHelper) {} + public async activateEnvironmentInTerminal( + terminal: Terminal, + options?: TerminalActivationOptions, + ): Promise<boolean> { if (this.activatedTerminals.has(terminal)) { return this.activatedTerminals.get(terminal)!; } const deferred = createDeferred<boolean>(); this.activatedTerminals.set(terminal, deferred.promise); - const shellPath = this.helper.getTerminalShellPath(); - const terminalShellType = !shellPath || shellPath.length === 0 ? TerminalShellType.other : this.helper.identifyTerminalShell(shellPath); + const terminalShellType = this.helper.identifyTerminalShell(terminal); - const activationCommamnds = await this.helper.getEnvironmentActivationCommands(terminalShellType, resource); + const activationCommands = await this.helper.getEnvironmentActivationCommands( + terminalShellType, + options?.resource, + options?.interpreter, + ); let activated = false; - if (activationCommamnds) { - for (const command of activationCommamnds!) { - terminal.show(preserveFocus); + if (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; @@ -32,7 +40,7 @@ export class BaseTerminalActivator implements ITerminalActivator { deferred.resolve(activated); return activated; } - protected async waitForCommandToProcess(shell: TerminalShellType) { + protected async waitForCommandToProcess(_shell: TerminalShellType) { // Give the command some time to complete. // Its been observed that sending commands too early will strip some text off in VS Code Terminal. await sleep(500); diff --git a/src/client/common/terminal/activator/index.ts b/src/client/common/terminal/activator/index.ts index 14bb67e710e3..cde04bdbf10d 100644 --- a/src/client/common/terminal/activator/index.ts +++ b/src/client/common/terminal/activator/index.ts @@ -4,20 +4,59 @@ 'use strict'; import { inject, injectable, multiInject } from 'inversify'; -import { Terminal, Uri } from 'vscode'; -import { ITerminalActivationHandler, ITerminalActivator, ITerminalHelper } from '../types'; +import { Terminal } from 'vscode'; +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; - constructor(@inject(ITerminalHelper) readonly helper: ITerminalHelper, - @multiInject(ITerminalActivationHandler) private readonly handlers: ITerminalActivationHandler[]) { + private pendingActivations = new WeakMap<Terminal, Promise<boolean>>(); + constructor( + @inject(ITerminalHelper) readonly helper: ITerminalHelper, + @multiInject(ITerminalActivationHandler) private readonly handlers: ITerminalActivationHandler[], + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + ) { this.initialize(); } - public async activateEnvironmentInTerminal(terminal: Terminal, resource: Uri | undefined, preserveFocus: boolean = true) { - const activated = await this.baseActivator.activateEnvironmentInTerminal(terminal, resource, preserveFocus); - this.handlers.forEach(handler => handler.handleActivation(terminal, resource, preserveFocus, activated).ignoreErrors()); + public async activateEnvironmentInTerminal( + terminal: Terminal, + options?: TerminalActivationOptions, + ): Promise<boolean> { + 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<boolean> { + const settings = this.configurationService.getSettings(options?.resource); + const activateEnvironment = + settings.terminal.activateEnvironment && !inTerminalEnvVarExperiment(this.experimentService); + if (!activateEnvironment || options?.hideFromUser || shouldEnvExtHandleActivation()) { + if (shouldEnvExtHandleActivation()) { + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL); + } + return false; + } + + const activated = await this.baseActivator.activateEnvironmentInTerminal(terminal, options); + this.handlers.forEach((handler) => + handler + .handleActivation(terminal, options?.resource, options?.preserveFocus === true, activated) + .ignoreErrors(), + ); return activated; } protected initialize() { diff --git a/src/client/common/terminal/activator/powershellFailedHandler.ts b/src/client/common/terminal/activator/powershellFailedHandler.ts index a8ecba9f6da0..d580ed4d38bf 100644 --- a/src/client/common/terminal/activator/powershellFailedHandler.ts +++ b/src/client/common/terminal/activator/powershellFailedHandler.ts @@ -4,32 +4,41 @@ 'use strict'; import { inject, injectable, named } from 'inversify'; -import { Terminal, Uri } from 'vscode'; -import { PowerShellActivationHackDiagnosticsServiceId, PowershellActivationNotAvailableDiagnostic } from '../../../application/diagnostics/checks/powerShellActivation'; +import { Terminal } from 'vscode'; +import { + PowerShellActivationHackDiagnosticsServiceId, + PowershellActivationNotAvailableDiagnostic, +} from '../../../application/diagnostics/checks/powerShellActivation'; import { IDiagnosticsService } from '../../../application/diagnostics/types'; import { IPlatformService } from '../../platform/types'; +import { Resource } from '../../types'; import { ITerminalActivationHandler, ITerminalHelper, TerminalShellType } from '../types'; @injectable() export class PowershellTerminalActivationFailedHandler implements ITerminalActivationHandler { - constructor(@inject(ITerminalHelper) private readonly helper: ITerminalHelper, + constructor( + @inject(ITerminalHelper) private readonly helper: ITerminalHelper, @inject(IPlatformService) private readonly platformService: IPlatformService, - @inject(IDiagnosticsService) @named(PowerShellActivationHackDiagnosticsServiceId) private readonly diagnosticService: IDiagnosticsService) { - } - public async handleActivation(_terminal: Terminal, resource: Uri | undefined, _preserveFocus: boolean, activated: boolean) { + @inject(IDiagnosticsService) + @named(PowerShellActivationHackDiagnosticsServiceId) + private readonly diagnosticService: IDiagnosticsService, + ) {} + public async handleActivation(terminal: Terminal, resource: Resource, _preserveFocus: boolean, activated: boolean) { if (activated || !this.platformService.isWindows) { return; } - const shell = this.helper.identifyTerminalShell(this.helper.getTerminalShellPath()); + const shell = this.helper.identifyTerminalShell(terminal); if (shell !== TerminalShellType.powershell && shell !== TerminalShellType.powershellCore) { return; } // Check if we can activate in Command Prompt. - const activationCommands = await this.helper.getEnvironmentActivationCommands(TerminalShellType.commandPrompt, resource); + const activationCommands = await this.helper.getEnvironmentActivationCommands( + TerminalShellType.commandPrompt, + resource, + ); if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) { return; } - this.diagnosticService.handle([new PowershellActivationNotAvailableDiagnostic()]).ignoreErrors(); + this.diagnosticService.handle([new PowershellActivationNotAvailableDiagnostic(resource)]).ignoreErrors(); } - } diff --git a/src/client/common/terminal/commandPrompt.ts b/src/client/common/terminal/commandPrompt.ts index aa4176dd2213..4a44557c52a7 100644 --- a/src/client/common/terminal/commandPrompt.ts +++ b/src/client/common/terminal/commandPrompt.ts @@ -17,7 +17,16 @@ export function getCommandPromptLocation(currentProcess: ICurrentProcess) { const system32Path = path.join(currentProcess.env.windir!, is32ProcessOn64Windows ? 'Sysnative' : 'System32'); return path.join(system32Path, 'cmd.exe'); } -export async function useCommandPromptAsDefaultShell(currentProcess: ICurrentProcess, configService: IConfigurationService) { +export async function useCommandPromptAsDefaultShell( + currentProcess: ICurrentProcess, + configService: IConfigurationService, +) { const cmdPromptLocation = getCommandPromptLocation(currentProcess); - await configService.updateSectionSetting('terminal', 'integrated.shell.windows', cmdPromptLocation, undefined, ConfigurationTarget.Global); + await configService.updateSectionSetting( + 'terminal', + 'integrated.shell.windows', + cmdPromptLocation, + undefined, + ConfigurationTarget.Global, + ); } diff --git a/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts index 2f2ce9532403..abc2ff89df63 100644 --- a/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts @@ -1,34 +1,96 @@ +/* eslint-disable max-classes-per-file */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { injectable } from 'inversify'; +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 { IFileSystem } from '../../platform/types'; -import { IConfigurationService } from '../../types'; import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; +type ExecutableFinderFunc = (python: string) => Promise<string | 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` + */ + +function getVenvExecutableFinder( + basename: string | string[], + // <path> + pathDirname: (filename: string) => string, + pathJoin: (...parts: string[]) => string, + // </path> + fileExists: (n: string) => Promise<boolean>, +): 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 { - constructor(protected readonly serviceContainer: IServiceContainer) { } +abstract class BaseActivationCommandProvider implements ITerminalActivationCommandProvider { + constructor(@inject(IServiceContainer) protected readonly serviceContainer: IServiceContainer) {} public abstract isShellSupported(targetShell: TerminalShellType): boolean; - public getActivationCommands(resource: Uri | undefined, targetShell: TerminalShellType): Promise<string[] | undefined> { - const pythonPath = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath; - return this.getActivationCommandsForInterpreter(pythonPath, targetShell); + + public async getActivationCommands( + resource: Uri | undefined, + targetShell: TerminalShellType, + ): Promise<string[] | undefined> { + const interpreter = await this.serviceContainer + .get<IInterpreterService>(IInterpreterService) + .getActiveInterpreter(resource); + if (!interpreter) { + return undefined; + } + return this.getActivationCommandsForInterpreter(interpreter.path, targetShell); } - public abstract getActivationCommandsForInterpreter(pythonPath: string, targetShell: TerminalShellType): Promise<string[] | undefined>; - protected async findScriptFile(pythonPath: string, scriptFileNames: string[]): Promise<string | undefined> { + public abstract getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType, + ): Promise<string[] | undefined>; +} + +export type ActivationScripts = Partial<Record<TerminalShellType, string[]>>; + +export abstract class VenvBaseActivationCommandProvider extends BaseActivationCommandProvider { + public isShellSupported(targetShell: TerminalShellType): boolean { + return this.scripts[targetShell] !== undefined; + } + + protected abstract get scripts(): ActivationScripts; + + protected async findScriptFile(pythonPath: string, targetShell: TerminalShellType): Promise<string | undefined> { const fs = this.serviceContainer.get<IFileSystem>(IFileSystem); - for (const scriptFileName of scriptFileNames) { - // Generate scripts are found in the same directory as the interpreter. - const scriptFile = path.join(path.dirname(pythonPath), scriptFileName); - const found = await fs.fileExists(scriptFile); - if (found) { - return scriptFile; - } + const candidates = this.scripts[targetShell]; + if (!candidates) { + return undefined; } + const findScript = getVenvExecutableFinder( + candidates, + path.dirname, + path.join, + // Bind "this"! + (n: string) => fs.fileExists(n), + ); + return findScript(pythonPath); } } diff --git a/src/client/common/terminal/environmentActivationProviders/bash.ts b/src/client/common/terminal/environmentActivationProviders/bash.ts index d330ec60d690..00c4d3da114c 100644 --- a/src/client/common/terminal/environmentActivationProviders/bash.ts +++ b/src/client/common/terminal/environmentActivationProviders/bash.ts @@ -1,54 +1,50 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable } from 'inversify'; -import { IServiceContainer } from '../../../ioc/types'; +import { injectable } from 'inversify'; import '../../extensions'; import { TerminalShellType } from '../types'; -import { BaseActivationCommandProvider } from './baseActivationProvider'; +import { ActivationScripts, VenvBaseActivationCommandProvider } from './baseActivationProvider'; -@injectable() -export class Bash extends BaseActivationCommandProvider { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer); - } - public isShellSupported(targetShell: TerminalShellType): boolean { - return targetShell === TerminalShellType.bash || - targetShell === TerminalShellType.gitbash || - targetShell === TerminalShellType.wsl || - targetShell === TerminalShellType.ksh || - targetShell === TerminalShellType.zsh || - targetShell === TerminalShellType.cshell || - targetShell === TerminalShellType.tcshell || - targetShell === TerminalShellType.fish; - } - public async getActivationCommandsForInterpreter(pythonPath: string, targetShell: TerminalShellType): Promise<string[] | undefined> { - const scriptFile = await this.findScriptFile(pythonPath, this.getScriptsInOrderOfPreference(targetShell)); - if (!scriptFile) { - return; +// For a given shell the scripts are in order of precedence. +const SCRIPTS: ActivationScripts = { + // Group 1 + [TerminalShellType.wsl]: ['activate.sh', 'activate'], + [TerminalShellType.ksh]: ['activate.sh', 'activate'], + [TerminalShellType.zsh]: ['activate.sh', 'activate'], + [TerminalShellType.gitbash]: ['activate.sh', 'activate'], + [TerminalShellType.bash]: ['activate.sh', 'activate'], + // Group 2 + [TerminalShellType.tcshell]: ['activate.csh'], + [TerminalShellType.cshell]: ['activate.csh'], + // Group 3 + [TerminalShellType.fish]: ['activate.fish'], +}; + +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 [`source ${scriptFile.fileToCommandArgument()}`]; } + return scripts; +} - private getScriptsInOrderOfPreference(targetShell: TerminalShellType): string[] { - switch (targetShell) { - case TerminalShellType.wsl: - case TerminalShellType.ksh: - case TerminalShellType.zsh: - case TerminalShellType.gitbash: - case TerminalShellType.bash: { - return ['activate.sh', 'activate']; - } - case TerminalShellType.tcshell: - case TerminalShellType.cshell: { - return ['activate.csh']; - } - case TerminalShellType.fish: { - return ['activate.fish']; - } - default: { - return []; - } +@injectable() +export class Bash extends VenvBaseActivationCommandProvider { + protected readonly scripts = SCRIPTS; + + public async getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType, + ): Promise<string[] | undefined> { + const scriptFile = await this.findScriptFile(pythonPath, targetShell); + if (!scriptFile) { + return undefined; } + return [`source ${scriptFile.fileToCommandArgumentForPythonExt()}`]; } } diff --git a/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts b/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts index 92b527030dbc..6d40e2c390a0 100644 --- a/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts +++ b/src/client/common/terminal/environmentActivationProviders/commandPrompt.ts @@ -6,44 +6,79 @@ import * as path from 'path'; import { IServiceContainer } from '../../../ioc/types'; import '../../extensions'; import { TerminalShellType } from '../types'; -import { BaseActivationCommandProvider } from './baseActivationProvider'; +import { ActivationScripts, VenvBaseActivationCommandProvider } from './baseActivationProvider'; + +// For a given shell the scripts are in order of precedence. +const SCRIPTS: ActivationScripts = { + // Group 1 + [TerminalShellType.commandPrompt]: ['activate.bat', 'Activate.ps1'], + // Group 2 + [TerminalShellType.powershell]: ['Activate.ps1', 'activate.bat'], + [TerminalShellType.powershellCore]: ['Activate.ps1', 'activate.bat'], +}; + +export function getAllScripts(pathJoin: (...p: string[]) => string): string[] { + const scripts: string[] = []; + 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), + ); + } + } + } + return scripts; +} @injectable() -export class CommandPromptAndPowerShell extends BaseActivationCommandProvider { +export class CommandPromptAndPowerShell extends VenvBaseActivationCommandProvider { + protected readonly scripts: ActivationScripts; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super(serviceContainer); + this.scripts = {}; + for (const [key, names] of Object.entries(SCRIPTS)) { + const shell = key as TerminalShellType; + const scripts: string[] = []; + for (const name of names) { + scripts.push( + name, + // We also add scripts in subdirs. + path.join('Scripts', name), + path.join('scripts', name), + ); + } + this.scripts[shell] = scripts; + } } - public isShellSupported(targetShell: TerminalShellType): boolean { - return targetShell === TerminalShellType.commandPrompt || - targetShell === TerminalShellType.powershell || - targetShell === TerminalShellType.powershellCore; - } - public async getActivationCommandsForInterpreter(pythonPath, targetShell: TerminalShellType): Promise<string[] | undefined> { - // Dependending on the target shell, look for the preferred script file. - const scriptFile = await this.findScriptFile(pythonPath, this.getScriptsInOrderOfPreference(targetShell)); + + public async getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType, + ): Promise<string[] | undefined> { + 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 ((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.powershell || targetShell === TerminalShellType.powershellCore) && + 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; } - } - private getScriptsInOrderOfPreference(targetShell: TerminalShellType): string[] { - const batchFiles = ['activate.bat', path.join('Scripts', 'activate.bat'), path.join('scripts', 'activate.bat')]; - const powerShellFiles = ['activate.ps1', path.join('Scripts', 'activate.ps1'), path.join('scripts', 'activate.ps1')]; - if (targetShell === TerminalShellType.commandPrompt) { - return batchFiles.concat(powerShellFiles); - } else { - return powerShellFiles.concat(batchFiles); - } + return undefined; } } diff --git a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts index a2087507f72c..42bb8f38fc9e 100644 --- a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts @@ -1,133 +1,172 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { ICondaService } from '../../../interpreter/contracts'; -import { IServiceContainer } from '../../../ioc/types'; -import '../../extensions'; -import { IPlatformService } from '../../platform/types'; -import { IConfigurationService } from '../../types'; -import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; - -/** - * Support conda env activation (in the terminal). - */ -@injectable() -export class CondaActivationCommandProvider implements ITerminalActivationCommandProvider { - constructor( - private readonly serviceContainer: IServiceContainer - ) { } - - /** - * Is the given shell supported for activating a conda env? - */ - public isShellSupported(_targetShell: TerminalShellType): boolean { - return true; - } - - /** - * Return the command needed to activate the conda env. - */ - public async getActivationCommands(resource: Uri | undefined, targetShell: TerminalShellType): Promise<string[] | undefined> { - const condaService = this.serviceContainer.get<ICondaService>(ICondaService); - const pythonPath = this.serviceContainer.get<IConfigurationService>(IConfigurationService) - .getSettings(resource).pythonPath; - - const envInfo = await condaService.getCondaEnvironment(pythonPath); - if (!envInfo) { - return; - } - - if (this.serviceContainer.get<IPlatformService>(IPlatformService).isWindows) { - // windows activate can be a bit tricky due to conda changes. - switch (targetShell) { - case TerminalShellType.powershell: - case TerminalShellType.powershellCore: - return this.getPowershellCommands(envInfo.name, targetShell); - - // tslint:disable-next-line:no-suspicious-comment - // TODO: Do we really special-case fish on Windows? - case TerminalShellType.fish: - return this.getFishCommands(envInfo.name, await condaService.getCondaFile()); - - default: - return this.getWindowsCommands(envInfo.name); - } - } else { - switch (targetShell) { - case TerminalShellType.powershell: - case TerminalShellType.powershellCore: - return; - - // tslint:disable-next-line:no-suspicious-comment - // TODO: What about pre-4.4.0? - case TerminalShellType.fish: - return this.getFishCommands(envInfo.name, await condaService.getCondaFile()); - - default: - return this.getUnixCommands( - envInfo.name, - await condaService.getCondaFile() - ); - } - } - } - - public async getWindowsActivateCommand(): Promise<string> { - let activateCmd: string = 'activate'; - - const condaService = this.serviceContainer.get<ICondaService>(ICondaService); - const condaExePath = await condaService.getCondaFile(); - - if (condaExePath && path.basename(condaExePath) !== condaExePath) { - const condaScriptsPath: string = path.dirname(condaExePath); - // prefix the cmd with the found path, and ensure it's quoted properly - activateCmd = path.join(condaScriptsPath, activateCmd); - activateCmd = activateCmd.toCommandArgument(); - } - - return activateCmd; - } - - public async getWindowsCommands( - envName: string - ): Promise<string[] | undefined> { - - const activate = await this.getWindowsActivateCommand(); - return [ - `${activate} ${envName.toCommandArgument()}` - ]; - } - - public async getPowershellCommands( - envName: string, - targetShell: TerminalShellType - ): Promise<string[] | undefined> { - return; - } - - public async getFishCommands( - envName: string, - conda: string - ): Promise<string[] | undefined> { - // https://github.com/conda/conda/blob/be8c08c083f4d5e05b06bd2689d2cd0d410c2ffe/shell/etc/fish/conf.d/conda.fish#L18-L28 - return [ - `${conda.fileToCommandArgument()} activate ${envName.toCommandArgument()}` - ]; - } - - public async getUnixCommands( - envName: string, - conda: string - ): Promise<string[] | undefined> { - const condaDir = path.dirname(conda); - const activateFile = path.join(condaDir, 'activate'); - return [ - `source ${activateFile.fileToCommandArgument()} ${envName.toCommandArgument()}` - ]; - } -} +// 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 { IComponentAdapter, ICondaService } from '../../../interpreter/contracts'; +import { IPlatformService } from '../../platform/types'; +import { IConfigurationService } from '../../types'; +import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; + +/** + * Support conda env activation (in the terminal). + */ +@injectable() +export class CondaActivationCommandProvider implements ITerminalActivationCommandProvider { + constructor( + @inject(ICondaService) private readonly condaService: ICondaService, + @inject(IPlatformService) private platform: IPlatformService, + @inject(IConfigurationService) private configService: IConfigurationService, + @inject(IComponentAdapter) private pyenvs: IComponentAdapter, + ) {} + + /** + * Is the given shell supported for activating a conda env? + */ + // eslint-disable-next-line class-methods-use-this + public isShellSupported(): boolean { + return true; + } + + /** + * Return the command needed to activate the conda env. + */ + public getActivationCommands( + resource: Uri | undefined, + targetShell: TerminalShellType, + ): Promise<string[] | undefined> { + const { pythonPath } = this.configService.getSettings(resource); + return this.getActivationCommandsForInterpreter(pythonPath, targetShell); + } + + /** + * Return the command needed to activate the conda env. + * + */ + public async getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType, + ): Promise<string[] | undefined> { + traceVerbose(`Getting conda activation commands for interpreter ${pythonPath} with shell ${targetShell}`); + const envInfo = await this.pyenvs.getCondaEnvironment(pythonPath); + if (!envInfo) { + 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; + + // 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 ( + this.platform.isWindows && + targetShell !== TerminalShellType.bash && + targetShell !== TerminalShellType.gitbash + ) { + const commands = [activatePath.path, `conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; + traceInfo(`Using Windows-specific commands: ${commands.join(', ')}`); + return commands; + } + + 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: + traceVerbose('Using PowerShell-specific activation'); + return _getPowershellCommands(condaEnv); + + // TODO: Do we really special-case fish on Windows? + case TerminalShellType.fish: + 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); + } + return getUnixCommands(condaEnv, await this.condaService.getCondaFile()); + } + } + + public async getWindowsActivateCommand(): Promise<string> { + let activateCmd = 'activate'; + + const condaExePath = await this.condaService.getCondaFile(); + + if (condaExePath && path.basename(condaExePath) !== condaExePath) { + const condaScriptsPath: string = path.dirname(condaExePath); + // prefix the cmd with the found path, and ensure it's quoted properly + activateCmd = path.join(condaScriptsPath, activateCmd); + activateCmd = activateCmd.toCommandArgumentForPythonExt(); + } + + return activateCmd; + } + + public async getWindowsCommands(condaEnv: string): Promise<string[] | undefined> { + const activate = await this.getWindowsActivateCommand(); + return [`${activate} ${condaEnv.toCommandArgumentForPythonExt()}`]; + } +} + +/** + * 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<string[] | undefined> { + return [`conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; +} + +async function getFishCommands(condaEnv: string, condaFile: string): Promise<string[] | undefined> { + // 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<string[] | undefined> { + 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<string[] | undefined> { + 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 new file mode 100644 index 000000000000..d097c759ec40 --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, named } from 'inversify'; +import { Uri } from 'vscode'; +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 { IToolExecutionPath, ToolExecutionPath } from '../../types'; +import { ITerminalActivationCommandProvider } from '../types'; + +@injectable() +export class PipEnvActivationCommandProvider implements ITerminalActivationCommandProvider { + constructor( + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IToolExecutionPath) + @named(ToolExecutionPath.pipenv) + private readonly pipEnvExecution: IToolExecutionPath, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + ) {} + + // eslint-disable-next-line class-methods-use-this + public isShellSupported(): boolean { + return false; + } + + public async getActivationCommands(resource: Uri | undefined): Promise<string[] | undefined> { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + 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) { + if (!(await isPipenvEnvironmentRelatedToFolder(interpreter.path, workspaceFolder?.uri.fsPath))) { + return undefined; + } + } + const execName = this.pipEnvExecution.executable; + return [`${execName.fileToCommandArgumentForPythonExt()} shell`]; + } + + public async getActivationCommandsForInterpreter(pythonPath: string): Promise<string[] | undefined> { + const interpreter = await this.interpreterService.getInterpreterDetails(pythonPath); + if (!interpreter || interpreter.envType !== EnvironmentType.Pipenv) { + return undefined; + } + + 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<string[] | undefined> { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (!interpreter) { + return undefined; + } + + return this.getActivationCommandsForInterpreter(interpreter.path, targetShell); + } + + public getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType, + ): Promise<string[] | undefined> { + 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 3d4efe717a21..6b5ced048672 100644 --- a/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts @@ -5,24 +5,42 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; -import { IInterpreterService, InterpreterType } from '../../../interpreter/contracts'; +import { IInterpreterService } from '../../../interpreter/contracts'; import { IServiceContainer } from '../../../ioc/types'; +import { EnvironmentType } from '../../../pythonEnvironments/info'; import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; @injectable() export class PyEnvActivationCommandProvider implements ITerminalActivationCommandProvider { - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { } + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} + // eslint-disable-next-line class-methods-use-this public isShellSupported(_targetShell: TerminalShellType): boolean { return true; } public async getActivationCommands(resource: Uri | undefined, _: TerminalShellType): Promise<string[] | undefined> { - const interpreter = await this.serviceContainer.get<IInterpreterService>(IInterpreterService).getActiveInterpreter(resource); - if (!interpreter || interpreter.type !== InterpreterType.Pyenv || !interpreter.envName) { - return; + const interpreter = await this.serviceContainer + .get<IInterpreterService>(IInterpreterService) + .getActiveInterpreter(resource); + if (!interpreter || interpreter.envType !== EnvironmentType.Pyenv || !interpreter.envName) { + return undefined; } - return [`pyenv shell ${interpreter.envName}`]; + return [`pyenv shell ${interpreter.envName.toCommandArgumentForPythonExt()}`]; + } + + public async getActivationCommandsForInterpreter( + pythonPath: string, + _targetShell: TerminalShellType, + ): Promise<string[] | undefined> { + const interpreter = await this.serviceContainer + .get<IInterpreterService>(IInterpreterService) + .getInterpreterDetails(pythonPath); + if (!interpreter || interpreter.envType !== EnvironmentType.Pyenv || !interpreter.envName) { + return undefined; + } + + return [`pyenv shell ${interpreter.envName.toCommandArgumentForPythonExt()}`]; } } diff --git a/src/client/common/terminal/factory.ts b/src/client/common/terminal/factory.ts index 5ae899603571..39cc88c4b024 100644 --- a/src/client/common/terminal/factory.ts +++ b/src/client/common/terminal/factory.ts @@ -3,39 +3,67 @@ 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 { PythonEnvironment } from '../../pythonEnvironments/info'; import { IWorkspaceService } from '../application/types'; +import { IFileSystem } from '../platform/types'; import { TerminalService } from './service'; -import { ITerminalService, ITerminalServiceFactory } from './types'; +import { SynchronousTerminalService } from './syncTerminalService'; +import { ITerminalService, ITerminalServiceFactory, TerminalCreationOptions } from './types'; @injectable() export class TerminalServiceFactory implements ITerminalServiceFactory { - private terminalServices: Map<string, ITerminalService>; + private terminalServices: Map<string, TerminalService>; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - - this.terminalServices = new Map<string, ITerminalService>(); + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IFileSystem) private fs: IFileSystem, + @inject(IInterpreterService) private interpreterService: IInterpreterService, + ) { + this.terminalServices = new Map<string, TerminalService>(); } - public getTerminalService(resource?: Uri, title?: string): ITerminalService { - - const terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; - const id = this.getTerminalId(terminalTitle, resource); + 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)) { - const terminalService = new TerminalService(this.serviceContainer, resource, terminalTitle); + 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); } - return this.terminalServices.get(id)!; + // Decorate terminal service with the synchronous service. + return new SynchronousTerminalService( + this.fs, + this.interpreterService, + this.terminalServices.get(id)!, + interpreter, + ); } public createTerminalService(resource?: Uri, title?: string): ITerminalService { - const terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; - return new TerminalService(this.serviceContainer, resource, terminalTitle); + title = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python'; + return new TerminalService(this.serviceContainer, { resource, title }); } - private getTerminalId(title: string, resource?: Uri): string { - if (!resource) { + private getTerminalId( + title: string, + resource?: Uri, + interpreter?: PythonEnvironment, + newTerminalPerFile?: boolean, + ): string { + if (!resource && !interpreter) { return title; } - const workspaceFolder = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService).getWorkspaceFolder(resource!); - return workspaceFolder ? `${title}:${workspaceFolder.uri.fsPath}` : title; + const workspaceFolder = this.serviceContainer + .get<IWorkspaceService>(IWorkspaceService) + .getWorkspaceFolder(resource || undefined); + 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 f119ed80cf85..d2b3bb7879af 100644 --- a/src/client/common/terminal/helper.ts +++ b/src/client/common/terminal/helper.ts @@ -1,107 +1,186 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable } from 'inversify'; +import { inject, injectable, multiInject, named } from 'inversify'; import { Terminal, Uri } from 'vscode'; -import { ICondaService } from '../../interpreter/contracts'; +import { IComponentAdapter, IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { ITerminalManager, IWorkspaceService } from '../application/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 { IPlatformService } from '../platform/types'; -import { IConfigurationService } from '../types'; -import { CondaActivationCommandProvider } from './environmentActivationProviders/condaActivationProvider'; -import { ITerminalActivationCommandProvider, ITerminalHelper, TerminalShellType } from './types'; - -// Types of shells can be found here: -// 1. https://wiki.ubuntu.com/ChangingShells -const IS_GITBASH = /(gitbash.exe$)/i; -const IS_BASH = /(bash.exe$|bash$)/i; -const IS_WSL = /(wsl.exe$)/i; -const IS_ZSH = /(zsh$)/i; -const IS_KSH = /(ksh$)/i; -const IS_COMMAND = /cmd.exe$/i; -const IS_POWERSHELL = /(powershell.exe$|powershell$)/i; -const IS_POWERSHELL_CORE = /(pwsh.exe$|pwsh$)/i; -const IS_FISH = /(fish$)/i; -const IS_CSHELL = /(csh$)/i; -const IS_TCSHELL = /(tcsh$)/i; +import { IConfigurationService, Resource } from '../types'; +import { OSType } from '../utils/platform'; +import { ShellDetector } from './shellDetector'; +import { + IShellDetector, + ITerminalActivationCommandProvider, + ITerminalHelper, + TerminalActivationProviders, + TerminalShellType, +} from './types'; +import { isPixiEnvironment } from '../../pythonEnvironments/common/environmentManagers/pixi'; @injectable() export class TerminalHelper implements ITerminalHelper { - private readonly detectableShells: Map<TerminalShellType, RegExp>; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.detectableShells = new Map<TerminalShellType, RegExp>(); - this.detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL); - this.detectableShells.set(TerminalShellType.gitbash, IS_GITBASH); - this.detectableShells.set(TerminalShellType.bash, IS_BASH); - this.detectableShells.set(TerminalShellType.wsl, IS_WSL); - this.detectableShells.set(TerminalShellType.zsh, IS_ZSH); - this.detectableShells.set(TerminalShellType.ksh, IS_KSH); - this.detectableShells.set(TerminalShellType.commandPrompt, IS_COMMAND); - this.detectableShells.set(TerminalShellType.fish, IS_FISH); - this.detectableShells.set(TerminalShellType.tcshell, IS_TCSHELL); - this.detectableShells.set(TerminalShellType.cshell, IS_CSHELL); - this.detectableShells.set(TerminalShellType.powershellCore, IS_POWERSHELL_CORE); + private readonly shellDetector: ShellDetector; + constructor( + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, + @inject(IInterpreterService) readonly interpreterService: IInterpreterService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.conda) + private readonly conda: ITerminalActivationCommandProvider, + @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.bashCShellFish) + private readonly bashCShellFish: ITerminalActivationCommandProvider, + @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.commandPromptAndPowerShell) + private readonly commandPromptAndPowerShell: ITerminalActivationCommandProvider, + @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.nushell) + private readonly nushell: ITerminalActivationCommandProvider, + @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.pyenv) + private readonly pyenv: ITerminalActivationCommandProvider, + @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.pipenv) + private readonly pipenv: ITerminalActivationCommandProvider, + @inject(ITerminalActivationCommandProvider) + @named(TerminalActivationProviders.pixi) + private readonly pixi: ITerminalActivationCommandProvider, + @multiInject(IShellDetector) shellDetectors: IShellDetector[], + ) { + this.shellDetector = new ShellDetector(this.platform, shellDetectors); } public createTerminal(title?: string): Terminal { - const terminalManager = this.serviceContainer.get<ITerminalManager>(ITerminalManager); - return terminalManager.createTerminal({ name: title }); + return this.terminalManager.createTerminal({ name: title }); } - public identifyTerminalShell(shellPath: string): TerminalShellType { - return Array.from(this.detectableShells.keys()) - .reduce((matchedShell, shellToDetect) => { - if (matchedShell === TerminalShellType.other && this.detectableShells.get(shellToDetect)!.test(shellPath)) { - return shellToDetect; - } - return matchedShell; - }, TerminalShellType.other); - } - public getTerminalShellPath(): string { - const workspace = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - const shellConfig = workspace.getConfiguration('terminal.integrated.shell'); - const platformService = this.serviceContainer.get<IPlatformService>(IPlatformService); - let osSection = ''; - if (platformService.isWindows) { - osSection = 'windows'; - } else if (platformService.isMac) { - osSection = 'osx'; - } else if (platformService.isLinux) { - osSection = 'linux'; - } - if (osSection.length === 0) { - return ''; - } - return shellConfig.get<string>(osSection)!; + public identifyTerminalShell(terminal?: Terminal): TerminalShellType { + return this.shellDetector.identifyTerminalShell(terminal); } + public buildCommandForTerminal(terminalShellType: TerminalShellType, command: string, args: string[]) { - const isPowershell = terminalShellType === TerminalShellType.powershell || terminalShellType === TerminalShellType.powershellCore; + const isPowershell = + terminalShellType === TerminalShellType.powershell || + terminalShellType === TerminalShellType.powershellCore; const commandPrefix = isPowershell ? '& ' : ''; - return `${commandPrefix}${command.fileToCommandArgument()} ${args.join(' ')}`.trim(); + const formattedArgs = args.map((a) => a.toCommandArgumentForPythonExt()); + + return `${commandPrefix}${command.fileToCommandArgumentForPythonExt()} ${formattedArgs.join(' ')}`.trim(); + } + public async getEnvironmentActivationCommands( + terminalShellType: TerminalShellType, + resource?: Uri, + interpreter?: PythonEnvironment, + ): Promise<string[] | undefined> { + 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, + ).ignoreErrors(); + return promise; } - public async getEnvironmentActivationCommands(terminalShellType: TerminalShellType, resource?: Uri): Promise<string[] | undefined> { - const settings = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource); - const activateEnvironment = settings.terminal.activateEnvironment; - if (!activateEnvironment) { + public async getEnvironmentActivationShellCommands( + resource: Resource, + shell: TerminalShellType, + interpreter?: PythonEnvironment, + ): Promise<string[] | undefined> { + if (this.platform.osType === OSType.Unknown) { return; } + 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, + ).ignoreErrors(); + return promise; + } + @traceDecoratorError('Failed to capture telemetry') + protected async sendTelemetry( + terminalShellType: TerminalShellType, + eventName: EventName, + interpreter: PythonEnvironment | undefined, + promise: Promise<string[] | undefined>, + ): Promise<void> { + let hasCommands = false; + let failed = false; + try { + const cmds = await promise; + hasCommands = Array.isArray(cmds) && cmds.length > 0; + } catch (ex) { + failed = true; + traceError('Failed to get activation commands', ex); + } + + const pythonVersion = interpreter && interpreter.version ? interpreter.version.raw : undefined; + const interpreterType = interpreter ? interpreter.envType : EnvironmentType.Unknown; + const data = { failed, hasCommands, interpreterType, terminal: terminalShellType, pythonVersion }; + sendTelemetryEvent(eventName, undefined, data); + } + protected async getActivationCommands( + resource: Resource, + interpreter: PythonEnvironment | undefined, + terminalShellType: TerminalShellType, + providers: ITerminalActivationCommandProvider[], + ): Promise<string[] | undefined> { + 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>(IComponentAdapter); // If we have a conda environment, then use that. - const isCondaEnvironment = await this.serviceContainer.get<ICondaService>(ICondaService).isCondaEnvironment(settings.pythonPath); + const isCondaEnvironment = interpreter + ? interpreter.envType === EnvironmentType.Conda + : await condaService.isCondaEnvironment(settings.pythonPath); if (isCondaEnvironment) { - const condaActivationProvider = new CondaActivationCommandProvider(this.serviceContainer); - const activationCommands = await condaActivationProvider.getActivationCommands(resource, terminalShellType); + const activationCommands = interpreter + ? await this.conda.getActivationCommandsForInterpreter(interpreter.path, terminalShellType) + : await this.conda.getActivationCommands(resource, terminalShellType); + if (Array.isArray(activationCommands)) { return activationCommands; } } // Search from the list of providers. - const providers = this.serviceContainer.getAll<ITerminalActivationCommandProvider>(ITerminalActivationCommandProvider); - const supportedProviders = providers.filter(provider => provider.isShellSupported(terminalShellType)); + const supportedProviders = providers.filter((provider) => provider.isShellSupported(terminalShellType)); for (const provider of supportedProviders) { - const activationCommands = await provider.getActivationCommands(resource, terminalShellType); - if (Array.isArray(activationCommands)) { + const activationCommands = interpreter + ? await provider.getActivationCommandsForInterpreter(interpreter.path, terminalShellType) + : await provider.getActivationCommands(resource, terminalShellType); + + if (Array.isArray(activationCommands) && activationCommands.length > 0) { return activationCommands; } } diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index 7e6e60d4a91d..0dffd5615ae1 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -2,15 +2,27 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Disposable, Event, EventEmitter, Terminal, Uri } 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 { TERMINAL_CREATE } from '../../telemetry/constants'; -import { ITerminalManager } from '../application/types'; +import { EventName } from '../../telemetry/constants'; +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, TerminalShellType } from './types'; +import { + ITerminalActivator, + ITerminalHelper, + ITerminalService, + TerminalCreationOptions, + 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 { @@ -20,69 +32,245 @@ 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<Disposable> = new Set(); + private _terminalFirstLaunched: boolean = true; + private pythonReplCommandQueue: string[] = []; + private isReplReady: boolean = false; + private replPromptListener?: Disposable; + private replShellTypeListener?: Disposable; public get onDidCloseTerminal(): Event<void> { return this.terminalClosed.event.bind(this.terminalClosed); } - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, - private resource?: Uri, - private title: string = 'Python') { + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + private readonly options?: TerminalCreationOptions, + ) { const disposableRegistry = this.serviceContainer.get<Disposable[]>(IDisposableRegistry); disposableRegistry.push(this); this.terminalHelper = this.serviceContainer.get<ITerminalHelper>(ITerminalHelper); this.terminalManager = this.serviceContainer.get<ITerminalManager>(ITerminalManager); + this.terminalAutoActivator = this.serviceContainer.get<ITerminalAutoActivation>(ITerminalAutoActivation); + this.applicationShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell); this.terminalManager.onDidCloseTerminal(this.terminalCloseHandler, this, disposableRegistry); this.terminalActivator = this.serviceContainer.get<ITerminalActivator>(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[]): Promise<void> { + public async sendCommand(command: string, args: string[], _?: CancellationToken): Promise<void> { await this.ensureTerminal(); const text = this.terminalHelper.buildCommandForTerminal(this.terminalShellType, command, args); - this.terminal!.show(true); - this.terminal!.sendText(text, true); + if (!this.options?.hideFromUser) { + this.terminal!.show(true); + } + + await this.executeCommand(text, false); } + /** @deprecated */ public async sendText(text: string): Promise<void> { await this.ensureTerminal(); - this.terminal!.show(true); + if (!this.options?.hideFromUser) { + this.terminal!.show(true); + } this.terminal!.sendText(text); } + public async executeCommand( + commandLine: string, + isPythonShell: boolean, + ): Promise<TerminalShellExecution | undefined> { + 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<TerminalShellExecution | undefined> { + 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<boolean>((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<void> { await this.ensureTerminal(preserveFocus); - this.terminal!.show(preserveFocus); + if (!this.options?.hideFromUser) { + this.terminal!.show(preserveFocus); + } } - private async ensureTerminal(preserveFocus: boolean = true): Promise<void> { + // TODO: Debt switch to Promise<Terminal> ---> breaks 20 tests + public async ensureTerminal(preserveFocus: boolean = true): Promise<void> { if (this.terminal) { return; } - const shellPath = this.terminalHelper.getTerminalShellPath(); - this.terminalShellType = !shellPath || shellPath.length === 0 ? TerminalShellType.other : this.terminalHelper.identifyTerminalShell(shellPath); - this.terminal = this.terminalManager.createTerminal({ name: this.title }); - // 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!, this.resource, preserveFocus); + await sleep(100); - this.terminal!.show(preserveFocus); + await this.terminalActivator.activateEnvironmentInTerminal(this.terminal, { + resource: this.options?.resource, + preserveFocus, + interpreter: this.options?.interpreter, + hideFromUser: this.options?.hideFromUser, + }); + } + + if (!this.options?.hideFromUser) { + this.terminal.show(preserveFocus); + } this.sendTelemetry().ignoreErrors(); + return; } private terminalCloseHandler(terminal: Terminal) { if (terminal === this.terminal) { this.terminalClosed.fire(); this.terminal = undefined; + this.isReplReady = false; + this.disposeReplListener(); + this.pythonReplCommandQueue = []; } } private async sendTelemetry() { - const pythonPath = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(this.resource).pythonPath; - const interpreterInfo = await this.serviceContainer.get<IInterpreterService>(IInterpreterService).getInterpreterDetails(pythonPath); - const pythonVersion = (interpreterInfo && interpreterInfo.version) ? interpreterInfo.version.raw : undefined; - const interpreterType = interpreterInfo ? interpreterInfo.type : undefined; - captureTelemetry(TERMINAL_CREATE, { terminal: this.terminalShellType, pythonVersion, interpreterType }); + const pythonPath = this.serviceContainer + .get<IConfigurationService>(IConfigurationService) + .getSettings(this.options?.resource).pythonPath; + const interpreterInfo = + this.options?.interpreter || + (await this.serviceContainer + .get<IInterpreterService>(IInterpreterService) + .getInterpreterDetails(pythonPath)); + const pythonVersion = interpreterInfo && interpreterInfo.version ? interpreterInfo.version.raw : undefined; + const interpreterType = interpreterInfo ? interpreterInfo.envType : undefined; + captureTelemetry(EventName.TERMINAL_CREATE, { + terminal: this.terminalShellType, + pythonVersion, + interpreterType, + }); } } diff --git a/src/client/common/terminal/shellDetector.ts b/src/client/common/terminal/shellDetector.ts new file mode 100644 index 000000000000..bf183f20a279 --- /dev/null +++ b/src/client/common/terminal/shellDetector.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable, multiInject } from 'inversify'; +import { Terminal, env } from 'vscode'; +import { traceError, traceVerbose } from '../../logging'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import '../extensions'; +import { IPlatformService } from '../platform/types'; +import { OSType } from '../utils/platform'; +import { IShellDetector, ShellIdentificationTelemetry, TerminalShellType } from './types'; + +const defaultOSShells = { + [OSType.Linux]: TerminalShellType.bash, + [OSType.OSX]: TerminalShellType.bash, + [OSType.Windows]: TerminalShellType.commandPrompt, + [OSType.Unknown]: TerminalShellType.other, +}; + +@injectable() +export class ShellDetector { + constructor( + @inject(IPlatformService) private readonly platform: IPlatformService, + @multiInject(IShellDetector) private readonly shellDetectors: IShellDetector[], + ) {} + /** + * Logic is as follows: + * 1. Try to identify the type of the shell based on the name of the terminal. + * 2. Try to identify the type of the shell based on the 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: https://github.com/microsoft/vscode/issues/74233#issuecomment-497527337 + */ + public identifyTerminalShell(terminal?: Terminal): TerminalShellType { + let shell: TerminalShellType | undefined; + const telemetryProperties: ShellIdentificationTelemetry = { + failed: true, + shellIdentificationSource: 'default', + terminalProvided: !!terminal, + hasCustomShell: undefined, + hasShellInEnv: undefined, + }; + + // Sort in order of priority and then identify the shell. + const shellDetectors = this.shellDetectors.slice().sort((a, b) => b.priority - a.priority); + + for (const detector of shellDetectors) { + shell = detector.identify(telemetryProperties, terminal); + if (shell && shell !== TerminalShellType.other) { + telemetryProperties.failed = false; + break; + } + } + + // This information is useful in determining how well we identify shells on users machines. + // This impacts executing code in terminals and activation of environments in terminal. + // So, the better this works, the better it is for the user. + sendTelemetryEvent(EventName.TERMINAL_SHELL_IDENTIFICATION, undefined, telemetryProperties); + traceVerbose(`Shell identified as ${shell} ${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]; + } + return shell; + } +} diff --git a/src/client/common/terminal/shellDetectors/baseShellDetector.ts b/src/client/common/terminal/shellDetectors/baseShellDetector.ts new file mode 100644 index 000000000000..4262bdf80364 --- /dev/null +++ b/src/client/common/terminal/shellDetectors/baseShellDetector.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable, unmanaged } from 'inversify'; +import { Terminal } from 'vscode'; +import { IShellDetector, ShellIdentificationTelemetry, TerminalShellType } from '../types'; + +/* +When identifying the shell use the following algorithm: +* 1. Identify shell based on the name of the terminal (if there is one already opened and used). +* 2. Identify shell based on the api provided by VSC. +* 2. Identify shell based on the settings in VSC. +* 3. Identify shell based on users environment variables. +* 4. Use default shells (bash for mac and linux, cmd for windows). +*/ + +// Types of shells can be found here: +// 1. https://wiki.ubuntu.com/ChangingShells +const IS_GITBASH = /(gitbash$)/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$)/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<TerminalShellType, RegExp>(); +detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL); +detectableShells.set(TerminalShellType.gitbash, IS_GITBASH); +detectableShells.set(TerminalShellType.bash, IS_BASH); +detectableShells.set(TerminalShellType.wsl, IS_WSL); +detectableShells.set(TerminalShellType.zsh, IS_ZSH); +detectableShells.set(TerminalShellType.ksh, IS_KSH); +detectableShells.set(TerminalShellType.commandPrompt, IS_COMMAND); +detectableShells.set(TerminalShellType.fish, IS_FISH); +detectableShells.set(TerminalShellType.tcshell, IS_TCSHELL); +detectableShells.set(TerminalShellType.cshell, IS_CSHELL); +detectableShells.set(TerminalShellType.nushell, IS_NUSHELL); +detectableShells.set(TerminalShellType.powershellCore, IS_POWERSHELL_CORE); +detectableShells.set(TerminalShellType.xonsh, IS_XONSH); + +@injectable() +export abstract class BaseShellDetector implements IShellDetector { + constructor(@unmanaged() public readonly priority: number) {} + public abstract identify( + telemetryProperties: ShellIdentificationTelemetry, + terminal?: Terminal, + ): TerminalShellType | undefined; + public identifyShellFromShellPath(shellPath: string): TerminalShellType { + 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 shell; +} diff --git a/src/client/common/terminal/shellDetectors/settingsShellDetector.ts b/src/client/common/terminal/shellDetectors/settingsShellDetector.ts new file mode 100644 index 000000000000..6288675ec3f8 --- /dev/null +++ b/src/client/common/terminal/shellDetectors/settingsShellDetector.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Terminal } from 'vscode'; +import { IWorkspaceService } from '../../application/types'; +import { IPlatformService } from '../../platform/types'; +import { OSType } from '../../utils/platform'; +import { ShellIdentificationTelemetry, TerminalShellType } from '../types'; +import { BaseShellDetector } from './baseShellDetector'; + +/** + * Identifies the shell based on the user settings. + */ +@injectable() +export class SettingsShellDetector extends BaseShellDetector { + constructor( + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + @inject(IPlatformService) private readonly platform: IPlatformService, + ) { + super(2); + } + public getTerminalShellPath(): string | undefined { + const shellConfig = this.workspace.getConfiguration('terminal.integrated.shell'); + let osSection = ''; + switch (this.platform.osType) { + case OSType.Windows: { + osSection = 'windows'; + break; + } + case OSType.OSX: { + osSection = 'osx'; + break; + } + case OSType.Linux: { + osSection = 'linux'; + break; + } + default: { + return ''; + } + } + return shellConfig.get<string>(osSection)!; + } + public identify( + telemetryProperties: ShellIdentificationTelemetry, + _terminal?: Terminal, + ): TerminalShellType | undefined { + const shellPath = this.getTerminalShellPath(); + telemetryProperties.hasCustomShell = !!shellPath; + const shell = shellPath ? this.identifyShellFromShellPath(shellPath) : TerminalShellType.other; + + if (shell !== TerminalShellType.other) { + telemetryProperties.shellIdentificationSource = 'environment'; + } else { + telemetryProperties.shellIdentificationSource = 'settings'; + } + return shell; + } +} diff --git a/src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts b/src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts new file mode 100644 index 000000000000..0f14adbe9d36 --- /dev/null +++ b/src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable } from 'inversify'; +import { Terminal } from 'vscode'; +import { traceVerbose } from '../../../logging'; +import { ShellIdentificationTelemetry, TerminalShellType } from '../types'; +import { BaseShellDetector } from './baseShellDetector'; + +/** + * Identifies the shell, based on the display name of the terminal. + */ +@injectable() +export class TerminalNameShellDetector extends BaseShellDetector { + constructor() { + super(4); + } + public identify( + telemetryProperties: ShellIdentificationTelemetry, + terminal?: Terminal, + ): TerminalShellType | undefined { + if (!terminal) { + return; + } + const shell = this.identifyShellFromShellPath(terminal.name); + traceVerbose(`Terminal name '${terminal.name}' identified as shell '${shell}'`); + telemetryProperties.shellIdentificationSource = + shell === TerminalShellType.other ? telemetryProperties.shellIdentificationSource : 'terminalName'; + return shell; + } +} diff --git a/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts b/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts new file mode 100644 index 000000000000..da84eef4d46f --- /dev/null +++ b/src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Terminal } from 'vscode'; +import { IPlatformService } from '../../platform/types'; +import { ICurrentProcess } from '../../types'; +import { OSType } from '../../utils/platform'; +import { ShellIdentificationTelemetry, TerminalShellType } from '../types'; +import { BaseShellDetector } from './baseShellDetector'; + +/** + * Identifies the shell based on the users environment (env variables). + */ +@injectable() +export class UserEnvironmentShellDetector extends BaseShellDetector { + constructor( + @inject(ICurrentProcess) private readonly currentProcess: ICurrentProcess, + @inject(IPlatformService) private readonly platform: IPlatformService, + ) { + super(1); + } + public getDefaultPlatformShell(): string { + return getDefaultShell(this.platform, this.currentProcess); + } + public identify( + telemetryProperties: ShellIdentificationTelemetry, + _terminal?: Terminal, + ): TerminalShellType | undefined { + const shellPath = this.getDefaultPlatformShell(); + telemetryProperties.hasShellInEnv = !!shellPath; + const shell = this.identifyShellFromShellPath(shellPath); + + if (shell !== TerminalShellType.other) { + telemetryProperties.shellIdentificationSource = 'environment'; + } + return shell; + } +} + +/* + The following code is based on VS Code from https://github.com/microsoft/vscode/blob/5c65d9bfa4c56538150d7f3066318e0db2c6151f/src/vs/workbench/contrib/terminal/node/terminal.ts#L12-L55 + This is only a fall back to identify the default shell used by VSC. + On Windows, determine the default shell. + On others, default to bash. +*/ +function getDefaultShell(platform: IPlatformService, currentProcess: ICurrentProcess): string { + if (platform.osType === OSType.Windows) { + return getTerminalDefaultShellWindows(platform, currentProcess); + } + + return currentProcess.env.SHELL && currentProcess.env.SHELL !== '/bin/false' + ? currentProcess.env.SHELL + : '/bin/bash'; +} +function getTerminalDefaultShellWindows(platform: IPlatformService, currentProcess: ICurrentProcess): string { + const isAtLeastWindows10 = parseFloat(platform.osRelease) >= 10; + const is32ProcessOn64Windows = currentProcess.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); + const powerShellPath = `${currentProcess.env.windir}\\${ + is32ProcessOn64Windows ? 'Sysnative' : 'System32' + }\\WindowsPowerShell\\v1.0\\powershell.exe`; + return isAtLeastWindows10 ? powerShellPath : getWindowsShell(currentProcess); +} + +function getWindowsShell(currentProcess: ICurrentProcess): string { + return currentProcess.env.comspec || 'cmd.exe'; +} diff --git a/src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts b/src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts new file mode 100644 index 000000000000..9ca1b8c4ec22 --- /dev/null +++ b/src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject } from 'inversify'; +import { Terminal } from 'vscode'; +import { traceVerbose } from '../../../logging'; +import { IApplicationEnvironment } from '../../application/types'; +import { ShellIdentificationTelemetry, TerminalShellType } from '../types'; +import { BaseShellDetector } from './baseShellDetector'; + +/** + * Identifies the shell, based on the VSC Environment API. + */ +export class VSCEnvironmentShellDetector extends BaseShellDetector { + constructor(@inject(IApplicationEnvironment) private readonly appEnv: IApplicationEnvironment) { + super(3); + } + public identify( + telemetryProperties: ShellIdentificationTelemetry, + terminal?: Terminal, + ): TerminalShellType | undefined { + const shellPath = + terminal?.creationOptions && 'shellPath' in terminal.creationOptions && terminal.creationOptions.shellPath + ? terminal.creationOptions.shellPath + : this.appEnv.shell; + if (!shellPath) { + return; + } + 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 new file mode 100644 index 000000000000..0b46a86ee51e --- /dev/null +++ b/src/client/common/terminal/syncTerminalService.ts @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject } from 'inversify'; +import { CancellationToken, Disposable, Event, TerminalShellExecution } from 'vscode'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { traceVerbose } from '../../logging'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { Cancellation } from '../cancellation'; +import { IFileSystem, TemporaryFile } from '../platform/types'; +import * as internalScripts from '../process/internal/scripts'; +import { createDeferred, Deferred } from '../utils/async'; +import { noop } from '../utils/misc'; +import { TerminalService } from './service'; +import { ITerminalService } from './types'; + +enum State { + notStarted = 0, + started = 1, + completed = 2, + errored = 4, +} + +class ExecutionState implements Disposable { + public state: State = State.notStarted; + private _completed: Deferred<void> = createDeferred(); + private disposable?: Disposable; + constructor( + public readonly lockFile: string, + private readonly fs: IFileSystem, + private readonly command: string[], + ) { + this.registerStateUpdate(); + this._completed.promise.finally(() => this.dispose()).ignoreErrors(); + } + public get completed(): Promise<void> { + return this._completed.promise; + } + public dispose() { + if (this.disposable) { + this.disposable.dispose(); + this.disposable = undefined; + } + } + private registerStateUpdate() { + const timeout = setInterval(async () => { + const state = await this.getLockFileState(this.lockFile); + if (state !== this.state) { + traceVerbose(`Command state changed to ${state}. ${this.command.join(' ')}`); + } + this.state = state; + if (state & State.errored) { + const errorContents = await this.fs.readFile(`${this.lockFile}.error`).catch(() => ''); + this._completed.reject( + new Error( + `Command failed with errors, check the terminal for details. Command: ${this.command.join( + ' ', + )}\n${errorContents}`, + ), + ); + } else if (state & State.completed) { + this._completed.resolve(); + } + }, 100); + + this.disposable = { + dispose: () => clearInterval(timeout as any), + }; + } + private async getLockFileState(file: string): Promise<State> { + const source = await this.fs.readFile(file); + let state: State = State.notStarted; + if (source.includes('START')) { + state |= State.started; + } + if (source.includes('END')) { + state |= State.completed; + } + if (source.includes('FAIL')) { + state |= State.completed | State.errored; + } + return state; + } +} + +/** + * This is a decorator class that ensures commands send to a terminal are completed and then execution is returned back to calling code. + * The tecnique used is simple: + * - Instead of sending actual text to a terminal, + * - Send text to a terminal that executes our python file, passing in the original text as args + * - The pthon file will execute the commands as a subprocess + * - At the end of the execution a file is created to singal completion. + */ +export class SynchronousTerminalService implements ITerminalService, Disposable { + private readonly disposables: Disposable[] = []; + public get onDidCloseTerminal(): Event<void> { + return this.terminalService.onDidCloseTerminal; + } + constructor( + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IInterpreterService) private readonly interpreter: IInterpreterService, + public readonly terminalService: TerminalService, + private readonly pythonInterpreter?: PythonEnvironment, + ) {} + public dispose() { + this.terminalService.dispose(); + while (this.disposables.length) { + const disposable = this.disposables.shift(); + if (disposable) { + try { + disposable.dispose(); + } catch { + noop(); + } + } else { + break; + } + } + } + public async sendCommand( + command: string, + args: string[], + cancel?: CancellationToken, + swallowExceptions: boolean = true, + ): Promise<void> { + if (!cancel) { + return this.terminalService.sendCommand(command, args); + } + const lockFile = await this.createLockFile(); + const state = new ExecutionState(lockFile.filePath, this.fs, [command, ...args]); + try { + const pythonExec = this.pythonInterpreter || (await this.interpreter.getActiveInterpreter(undefined)); + const sendArgs = internalScripts.shell_exec(command, lockFile.filePath, args); + await this.terminalService.sendCommand(pythonExec?.path || 'python', sendArgs); + const promise = swallowExceptions ? state.completed : state.completed.catch(noop); + await Cancellation.race(() => promise, cancel); + } finally { + state.dispose(); + lockFile.dispose(); + } + } + /** @deprecated */ + public sendText(text: string): Promise<void> { + return this.terminalService.sendText(text); + } + public executeCommand(commandLine: string, isPythonShell: boolean): Promise<TerminalShellExecution | undefined> { + return this.terminalService.executeCommand(commandLine, isPythonShell); + } + public show(preserveFocus?: boolean | undefined): Promise<void> { + return this.terminalService.show(preserveFocus); + } + + private createLockFile(): Promise<TemporaryFile> { + return this.fs.createTemporaryFile('.log').then((l) => { + this.disposables.push(l); + return l; + }); + } +} diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts index aebf8758b7fc..3e54458a57fd 100644 --- a/src/client/common/terminal/types.ts +++ b/src/client/common/terminal/types.ts @@ -1,9 +1,22 @@ - // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Event, Terminal, Uri } from 'vscode'; +'use strict'; + +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', + pixi = 'pixi', +} export enum TerminalShellType { powershell = 'powershell', powershellCore = 'powershellCore', @@ -15,29 +28,75 @@ export enum TerminalShellType { fish = 'fish', cshell = 'cshell', tcshell = 'tshell', + nushell = 'nushell', wsl = 'wsl', - other = 'other' + xonsh = 'xonsh', + other = 'other', } -export interface ITerminalService { +export interface ITerminalService extends IDisposable { readonly onDidCloseTerminal: Event<void>; - sendCommand(command: string, args: string[]): Promise<void>; + /** + * Sends a command to the terminal. + * + * @param {string} command + * @param {string[]} args + * @param {CancellationToken} [cancel] If provided, then wait till the command is executed in the terminal. + * @param {boolean} [swallowExceptions] Whether to swallow exceptions raised as a result of the execution of the command. Defaults to `true`. + * @returns {Promise<void>} + * @memberof ITerminalService + */ + sendCommand( + command: string, + args: string[], + cancel?: CancellationToken, + swallowExceptions?: boolean, + ): Promise<void>; + /** @deprecated */ sendText(text: string): Promise<void>; + executeCommand(commandLine: string, isPythonShell: boolean): Promise<TerminalShellExecution | undefined>; show(preserveFocus?: boolean): Promise<void>; } export const ITerminalServiceFactory = Symbol('ITerminalServiceFactory'); +export type TerminalCreationOptions = { + /** + * Object with environment variables that will be added to the Terminal. + */ + env?: { [key: string]: string | null }; + /** + * Resource identifier. E.g. used to determine python interpreter that needs to be used or environment variables or the like. + * + * @type {Uri} + */ + resource?: Uri; + /** + * Title. + * + * @type {string} + */ + title?: string; + /** + * Associated Python Interpreter. + * + * @type {PythonEnvironment} + */ + interpreter?: PythonEnvironment; + /** + * Whether hidden. + * + * @type {boolean} + */ + hideFromUser?: boolean; +}; + export interface ITerminalServiceFactory { /** - * Gets a terminal service with a specific title. - * If one exists, its returned else a new one is created. - * @param {Uri} resource - * @param {string} title - * @returns {ITerminalService} - * @memberof ITerminalServiceFactory + * Gets a terminal service. + * If one exists with the same information, that is returned else a new one is created. */ - getTerminalService(resource?: Uri, title?: string): ITerminalService; + getTerminalService(options: TerminalCreationOptions & { newTerminalPerFile?: boolean }): ITerminalService; createTerminalService(resource?: Uri, title?: string): ITerminalService; } @@ -45,15 +104,30 @@ export const ITerminalHelper = Symbol('ITerminalHelper'); export interface ITerminalHelper { createTerminal(title?: string): Terminal; - identifyTerminalShell(shellPath: string): TerminalShellType; - getTerminalShellPath(): string; + identifyTerminalShell(terminal?: Terminal): TerminalShellType; buildCommandForTerminal(terminalShellType: TerminalShellType, command: string, args: string[]): string; - getEnvironmentActivationCommands(terminalShellType: TerminalShellType, resource?: Uri): Promise<string[] | undefined>; + getEnvironmentActivationCommands( + terminalShellType: TerminalShellType, + resource?: Uri, + interpreter?: PythonEnvironment, + ): Promise<string[] | undefined>; + getEnvironmentActivationShellCommands( + resource: Resource, + shell: TerminalShellType, + interpreter?: PythonEnvironment, + ): Promise<string[] | undefined>; } export const ITerminalActivator = Symbol('ITerminalActivator'); +export type TerminalActivationOptions = { + resource?: Resource; + preserveFocus?: boolean; + interpreter?: PythonEnvironment; + // When sending commands to the terminal, do not display the terminal. + hideFromUser?: boolean; +}; export interface ITerminalActivator { - activateEnvironmentInTerminal(terminal: Terminal, resource: Uri | undefined, preserveFocus?: boolean): Promise<boolean>; + activateEnvironmentInTerminal(terminal: Terminal, options?: TerminalActivationOptions): Promise<boolean>; } export const ITerminalActivationCommandProvider = Symbol('ITerminalActivationCommandProvider'); @@ -61,10 +135,33 @@ export const ITerminalActivationCommandProvider = Symbol('ITerminalActivationCom export interface ITerminalActivationCommandProvider { isShellSupported(targetShell: TerminalShellType): boolean; getActivationCommands(resource: Uri | undefined, targetShell: TerminalShellType): Promise<string[] | undefined>; - getActivationCommandsForInterpreter?(pythonPath, targetShell: TerminalShellType): Promise<string[] | undefined>; + getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType, + ): Promise<string[] | undefined>; } export const ITerminalActivationHandler = Symbol('ITerminalActivationHandler'); export interface ITerminalActivationHandler { - handleActivation(terminal: Terminal, resource: Uri | undefined, preserveFocus: boolean, activated: boolean): Promise<void>; + handleActivation( + terminal: Terminal, + resource: Uri | undefined, + preserveFocus: boolean, + activated: boolean, + ): Promise<void>; +} + +export type ShellIdentificationTelemetry = IEventNamePropertyMapping['TERMINAL_SHELL_IDENTIFICATION']; + +export const IShellDetector = Symbol('IShellDetector'); +/** + * Used to identify a shell. + * Each implemenetion will provide a unique way of identifying the shell. + */ +export interface IShellDetector { + /** + * Classes with higher priorities will be used first when identifying the shell. + */ + 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 3d8ad02b97a1..c30ad704b6c1 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -1,32 +1,55 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; import { Socket } from 'net'; -import { ConfigurationTarget, DiagnosticSeverity, Disposable, Extension, ExtensionContext, OutputChannel, Uri, WorkspaceEdit } from 'vscode'; +import { + CancellationToken, + ConfigurationChangeEvent, + ConfigurationTarget, + Disposable, + DocumentSymbolProvider, + Event, + Extension, + ExtensionContext, + Memento, + LogOutputChannel, + Uri, +} from 'vscode'; +import { LanguageServerType } from '../activation/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<void>; +} + +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('IDiposableRegistry'); -export type IDisposableRegistry = { push(disposable: Disposable): void }; +export const IDisposableRegistry = Symbol('IDisposableRegistry'); +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<T> { + /** + * 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<void>; } -export type Version = { - raw: string; - major: number; - minor: number; - patch: number; - build: string[]; - prerelease: string[]; + +export type ReadWrite<T> = { + -readonly [P in keyof T]: T[P]; }; export const IPersistentStateFactory = Symbol('IPersistentStateFactory'); @@ -41,72 +64,66 @@ export type ExecutionInfo = { moduleName?: string; args: string[]; product?: Product; + useShell?: boolean; }; -export enum LogLevel { - Information = 'Information', - Error = 'Error', - Warning = 'Warning' -} - -export const ILogger = Symbol('ILogger'); - -export interface ILogger { - logError(message: string, error?: Error); - logWarning(message: string, error?: Error); - logInformation(message: string, error?: Error); -} - 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', + Python = 'Python', } export enum Product { pytest = 1, - nosetest = 2, - pylint = 3, - flake8 = 4, - pep8 = 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 -} - -export enum ModuleNamePurpose { - install = 1, - run = 2 + tensorboard = 24, + torchProfilerInstallName = 25, + torchProfilerImportName = 26, + pip = 27, + ensurepip = 28, + python = 29, } export const IInstaller = Symbol('IInstaller'); export interface IInstaller { - promptToInstall(product: Product, resource?: Uri): Promise<InstallerResponse>; - install(product: Product, resource?: Uri): Promise<InstallerResponse>; - isInstalled(product: Product, resource?: Uri): Promise<boolean | undefined>; - translateProductToModuleName(product: Product, purpose: ModuleNamePurpose): string; -} - + promptToInstall( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + ): Promise<InstallerResponse>; + install( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, + ): Promise<InstallerResponse>; + isInstalled(product: Product, resource?: InterpreterUri): Promise<boolean>; + isProductVersionCompatible( + product: Product, + semVerRequirement: string, + resource?: InterpreterUri, + ): Promise<ProductInstallStatus>; + translateProductToModuleName(product: Product): string; +} + +// TODO: Drop IPathUtils in favor of IFileSystemPathUtils. +// See https://github.com/microsoft/vscode-python/issues/8542. export const IPathUtils = Symbol('IPathUtils'); - export interface IPathUtils { readonly delimiter: string; readonly home: string; @@ -133,168 +150,100 @@ 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 downloadLanguageServer: boolean; - readonly jediEnabled: boolean; - readonly jediPath: string; - readonly jediMemoryLimit: number; + readonly pipenvPath: string; + readonly poetryPath: string; + readonly pixiToolPath: string; readonly devOptions: string[]; - readonly linting: ILintingSettings; - readonly formatting: IFormattingSettings; - readonly unitTest: IUnitTestSettings; + 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; -} -export interface ISortImportSettings { - readonly path: string; - readonly args: string[]; + readonly experiments: IExperiments; + readonly languageServer: LanguageServerType; + readonly languageServerIsDefault: boolean; + readonly defaultInterpreterPath: string; + readonly REPL: IREPLSettings; + register(): void; } -export interface IUnitTestSettings { - 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 IPep8CategorySeverity { - readonly W: DiagnosticSeverity; - readonly E: DiagnosticSeverity; -} -// tslint:disable-next-line:interface-name -export interface Flake8CategorySeverity { - readonly F: DiagnosticSeverity; - readonly E: DiagnosticSeverity; - readonly W: DiagnosticSeverity; -} -export interface IMypyCategorySeverity { - readonly error: DiagnosticSeverity; - readonly note: DiagnosticSeverity; -} -export interface ILintingSettings { - readonly enabled: boolean; - readonly ignorePatterns: string[]; - readonly prospectorEnabled: boolean; - readonly prospectorArgs: string[]; - readonly pylintEnabled: boolean; - readonly pylintArgs: string[]; - readonly pep8Enabled: boolean; - readonly pep8Args: 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 pep8CategorySeverity: IPep8CategorySeverity; - readonly flake8CategorySeverity: Flake8CategorySeverity; - readonly mypyCategorySeverity: IMypyCategorySeverity; - prospectorPath: string; - pylintPath: string; - pep8Path: 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 IInterpreterSettings { + infoVisibility: 'never' | 'onPythonRelated' | 'always'; } + 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 type LanguageServerDownloadChannels = 'stable' | 'beta' | 'daily'; -export interface IAnalysisSettings { - readonly downloadChannel?: LanguageServerDownloadChannels; - readonly openFilesOnly: boolean; - readonly typeshedPaths: string[]; - readonly errors: string[]; - readonly warnings: string[]; - readonly information: string[]; - readonly disabled: string[]; - readonly traceLogging: boolean; - readonly logLevel: LogLevel; +export interface IExperiments { + /** + * Return `true` if experiments are enabled, else `false`. + */ + readonly enabled: boolean; + /** + * Experiments user requested to opt into manually + */ + readonly optInto: string[]; + /** + * Experiments user requested to opt out from manually + */ + readonly optOutFrom: string[]; } -export interface IDataScienceSettings { - allowImportFromNotebook: boolean; - enabled: boolean; - jupyterInterruptTimeout: number; - jupyterLaunchTimeout: number; - jupyterServerURI: string; - notebookFileRoot: string; - changeDirOnImportExport: boolean; - useDefaultConfigForJupyter: boolean; - searchForJupyter: boolean; +export interface IAutoCompleteSettings { + readonly extraPaths: string[]; } export const IConfigurationService = Symbol('IConfigurationService'); export interface IConfigurationService { + readonly onDidChange: Event<ConfigurationChangeEvent | undefined>; getSettings(resource?: Uri): IPythonSettings; isTestExecution(): boolean; - updateSetting(setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise<void>; - updateSectionSetting(section: string, setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise<void>; + updateSetting(setting: string, value?: unknown, resource?: Uri, configTarget?: ConfigurationTarget): Promise<void>; + updateSectionSetting( + section: string, + setting: string, + value?: unknown, + resource?: Uri, + configTarget?: ConfigurationTarget, + ): Promise<void>; +} + +/** + * 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'); @@ -303,16 +252,37 @@ export interface ISocketServer extends Disposable { Start(options?: { port?: number; host?: string }): Promise<number>; } +export type DownloadOptions = { + /** + * Prefix for progress messages displayed. + * + * @type {('Downloading ... ' | string)} + */ + progressMessagePrefix: 'Downloading ... ' | string; + /** + * Extension of file that'll be created when downloading the file. + * + * @type {('tmp' | string)} + */ + extension: 'tmp' | string; +}; + export const IExtensionContext = Symbol('ExtensionContext'); -export interface IExtensionContext extends ExtensionContext { } +export interface IExtensionContext extends ExtensionContext {} export const IExtensions = Symbol('IExtensions'); export interface IExtensions { /** * All extensions currently known to the system. */ - // tslint:disable-next-line:no-any - readonly all: Extension<any>[]; + + readonly all: readonly Extension<unknown>[]; + + /** + * An event which fires when `extensions.all` changes. This can happen when extensions are + * installed, uninstalled, enabled or disabled. + */ + readonly onDidChange: Event<void>; /** * Get an extension by its full identifier in the form of: `publisher.name`. @@ -320,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<any> | undefined; + + getExtension(extensionId: string): Extension<unknown> | undefined; /** * Get an extension its full identifier in the form of: `publisher.name`. @@ -330,6 +300,11 @@ export interface IExtensions { * @return An extension or `undefined`. */ getExtension<T>(extensionId: string): Extension<T> | undefined; + + /** + * Determines which extension called into our extension code based on call stacks. + */ + determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }>; } export const IBrowserService = Symbol('IBrowserService'); @@ -337,49 +312,55 @@ export interface IBrowserService { launch(url: string): void; } -export const IPythonExtensionBanner = Symbol('IPythonExtensionBanner'); -export interface IPythonExtensionBanner { - enabled: boolean; - shownCount: Promise<number>; - optionLabels: string[]; - showBanner(): Promise<void>; -} -export const BANNER_NAME_LS_SURVEY: string = 'LSSurveyBanner'; -export const BANNER_NAME_PROPOSE_LS: string = 'ProposeLS'; -export const BANNER_NAME_DS_SURVEY: string = 'DSSurveyBanner'; - -export type DeprecatedSettingAndValue = { - setting: string; - values?: {}[]; -}; - -export type DeprecatedFeatureInfo = { - doNotDisplayPromptStateKey: string; - message: string; - moreInfoUrl: string; - commands?: string[]; - setting?: DeprecatedSettingAndValue; -}; - -export const IFeatureDeprecationManager = Symbol('IFeatureDeprecationManager'); - -export interface IFeatureDeprecationManager extends Disposable { - initialize(): void; - registerDeprecation(deprecatedInfo: DeprecatedFeatureInfo): void; +/** + * Stores hash formats + */ +export interface IHashFormat { + number: number; // If hash format is a number + string: string; // If hash format is a string } -export const IEditorUtils = Symbol('IEditorUtils'); -export interface IEditorUtils { - // getTextEditor(uri: Uri): Promise<{ editor: TextEditor; dispose?(): void }>; - getWorkspaceEditsFromPatch(originalContents: string, patch: string, uri: Uri): WorkspaceEdit; +/** + * Experiment service leveraging VS Code's experiment framework. + */ +export const IExperimentService = Symbol('IExperimentService'); +export interface IExperimentService { + activate(): Promise<void>; + inExperiment(experimentName: string): Promise<boolean>; + inExperimentSync(experimentName: string): boolean; + getExperimentValue<T extends boolean | number | string>(experimentName: string): Promise<T | undefined>; } -export interface IDisposable { - dispose(): Promise<void> | undefined | void; -} +export type InterpreterConfigurationScope = { uri: Resource; configTarget: ConfigurationTarget }; +export type InspectInterpreterSettingType = { + globalValue?: string; + workspaceValue?: string; + workspaceFolderValue?: string; +}; -export const IAsyncDisposableRegistry = Symbol('IAsyncDisposableRegistry'); -export interface IAsyncDisposableRegistry { - dispose(): Promise<void>; - push(disposable: IDisposable); +/** + * Interface used to access current Interpreter Path + */ +export const IInterpreterPathService = Symbol('IInterpreterPathService'); +export interface IInterpreterPathService { + onDidChange: Event<InterpreterConfigurationScope>; + get(resource: Resource): string; + inspect(resource: Resource): InspectInterpreterSettingType; + update(resource: Resource, configTarget: ConfigurationTarget, value: string | undefined): Promise<void>; + copyOldInterpreterStorageValuesToNew(resource: Resource): Promise<void>; +} + +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<T>(arr: T[], asyncPredicate: (value: T) => Promise<unknown>): Promise<T[]> { + const results = await Promise.all(arr.map(asyncPredicate)); + return arr.filter((_v, index) => results[index]); +} + +export async function asyncForEach<T>(arr: T[], asyncFunc: (value: T) => Promise<void>): Promise<void> { + await Promise.all(arr.map(asyncFunc)); +} diff --git a/src/client/common/utils/async.ts b/src/client/common/utils/async.ts index e7f8d53700e9..a44425f8f1a3 100644 --- a/src/client/common/utils/async.ts +++ b/src/client/common/utils/async.ts @@ -1,86 +1,293 @@ +/* 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. 'use strict'; -export async function sleep(timeout: number) { - return new Promise((resolve) => { - setTimeout(resolve, timeout); +export async function sleep(timeout: number): Promise<number> { + return new Promise<number>((resolve) => { + setTimeout(() => resolve(timeout), timeout); }); } -//====================== +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export function isThenable<T>(v: any): v is Thenable<T> { + return typeof v?.then === 'function'; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export function isPromise<T>(v: any): v is Promise<T> { + return typeof v?.then === 'function' && typeof v?.catch === 'function'; +} + // Deferred -// tslint:disable-next-line:interface-name export interface Deferred<T> { readonly promise: Promise<T>; readonly resolved: boolean; readonly rejected: boolean; readonly completed: boolean; - resolve(value?: T | PromiseLike<T>); - // tslint:disable-next-line:no-any - reject(reason?: any); + resolve(value?: T | PromiseLike<T>): void; + reject(reason?: string | Error | Record<string, unknown> | unknown): void; } class DeferredImpl<T> implements Deferred<T> { - private _resolve!: (value?: T | PromiseLike<T>) => 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<T>) => void; + + private _reject!: (reason?: string | Error | Record<string, unknown>) => void; + + private _resolved = false; + + private _rejected = false; + private _promise: Promise<T>; - // 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<T>((res, rej) => { this._resolve = res; this._reject = rej; }); } - public resolve(value?: T | PromiseLike<T>) { - // tslint:disable-next-line:no-any - this._resolve.apply(this.scope ? this.scope : this, arguments as any); + + public resolve(_value: T | PromiseLike<T>) { + 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<string, unknown>) { + if (this.completed) { + return; + } + this._reject.apply(this.scope ? this.scope : this, [_reason]); this._rejected = true; } + get promise(): Promise<T> { 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<T>(scope: any = null): Deferred<T> { + +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export function createDeferred<T = void>(scope: any = null): Deferred<T> { return new DeferredImpl<T>(scope); } export function createDeferredFrom<T>(...promises: Promise<T>[]): Deferred<T> { const deferred = createDeferred<T>(); Promise.all<T>(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; } export function createDeferredFromPromise<T>(promise: Promise<T>): Deferred<T> { const deferred = createDeferred<T>(); - promise - .then(deferred.resolve.bind(deferred)) - .catch(deferred.reject.bind(deferred)); + promise.then(deferred.resolve.bind(deferred)).catch(deferred.reject.bind(deferred)); return deferred; } + +// iterators + +interface IAsyncIterator<T> extends AsyncIterator<T, void> {} + +export interface IAsyncIterableIterator<T> extends IAsyncIterator<T>, AsyncIterable<T> {} + +/** + * An iterator that yields nothing. + */ +export function iterEmpty<T>(): IAsyncIterableIterator<T> { + return ((async function* () { + /** No body. */ + })() as unknown) as IAsyncIterableIterator<T>; +} + +type NextResult<T> = { index: number } & ( + | { result: IteratorResult<T, T | void>; err: null } + | { result: null; err: Error } +); +async function getNext<T>(it: AsyncIterator<T, T | void>, indexMaybe?: number): Promise<NextResult<T>> { + 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<unknown> = 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<T>( + iterators: AsyncIterator<T, T | void>[], + onError?: (err: Error, index: number) => Promise<void>, + // Ultimately we may also want to support cancellation. +): IAsyncIterableIterator<T> { + 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<NextResult<T>>; + numRunning -= 1; + if (onError !== undefined) { + await onError(err, index); + } + // XXX Log the error. + } else if (result!.done) { + promises[index] = NEVER as Promise<NextResult<T>>; + 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<T, R = T>( + items: T[], + func: (item: T) => Promise<R>, + race = true, +): IAsyncIterableIterator<R> { + 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<T>(iterator: IAsyncIterator<T>): IAsyncIterableIterator<T> { + const it = iterator as IAsyncIterableIterator<T>; + if (it[Symbol.asyncIterator] === undefined) { + it[Symbol.asyncIterator] = () => it; + } + return it; +} + +/** + * Get everything yielded by the iterator. + */ +export async function flattenIterator<T>(iterator: IAsyncIterator<T>): Promise<T[]> { + 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<T>(iterableItem: AsyncIterable<T>): Promise<T[]> { + 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<boolean>, + timeoutMs: number, + errorMessage: string, +): Promise<void> { + return new Promise<void>(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<T>(v: any): v is PromiseLike<T> { + return typeof v?.then === 'function'; +} + +export function raceTimeout<T>(timeout: number, ...promises: Promise<T>[]): Promise<T | undefined>; +export function raceTimeout<T>(timeout: number, defaultValue: T, ...promises: Promise<T>[]): Promise<T>; +export function raceTimeout<T>(timeout: number, defaultValue: T, ...promises: Promise<T>[]): Promise<T> { + const resolveValue = isPromiseLike(defaultValue) ? undefined : defaultValue; + if (isPromiseLike(defaultValue)) { + promises.push((defaultValue as unknown) as Promise<T>); + } + + 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<T>((resolve) => (promiseResolve = resolve)), + ]); +} diff --git a/src/client/common/utils/cacheUtils.ts b/src/client/common/utils/cacheUtils.ts new file mode 100644 index 000000000000..6101b3ef928f --- /dev/null +++ b/src/client/common/utils/cacheUtils.ts @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const globalCacheStore = new Map<string, { expiry: number; data: any }>(); + +// Gets a cache store to be used to store return values of methods or any other. +export function getGlobalCacheStore() { + return globalCacheStore; +} + +export function getCacheKeyFromFunctionArgs(keyPrefix: string, fnArgs: any[]): string { + const argsKey = fnArgs.map((arg) => `${JSON.stringify(arg)}`).join('-Arg-Separator-'); + return `KeyPrefix=${keyPrefix}-Args=${argsKey}`; +} + +export function clearCache() { + globalCacheStore.clear(); +} + +type CacheData<T> = { + value: T; + expiry: number; +}; + +/** + * InMemoryCache caches a single value up until its expiry. + */ +export class InMemoryCache<T> { + private cacheData?: CacheData<T>; + + constructor(protected readonly expiryDurationMs: number) {} + public get hasData() { + if (!this.cacheData || this.hasExpired(this.cacheData.expiry)) { + this.cacheData = undefined; + return false; + } + return true; + } + /** + * Returns undefined if there is no data. + * Uses `hasData` to determine whether any cached data exists. + * + * @readonly + * @type {(T | undefined)} + * @memberof InMemoryCache + */ + public get data(): T | undefined { + if (!this.hasData) { + return; + } + return this.cacheData?.value; + } + public set data(value: T | undefined) { + if (value !== undefined) { + this.cacheData = { + expiry: this.calculateExpiry(), + value, + }; + } else { + this.cacheData = undefined; + } + } + public clear() { + this.cacheData = undefined; + } + + /** + * Has this data expired? + * (protected class member to allow for reliable non-data-time-based testing) + * + * @param expiry The date to be tested for expiry. + * @returns true if the data expired, false otherwise. + */ + protected hasExpired(expiry: number): boolean { + return expiry <= Date.now(); + } + + /** + * When should this data item expire? + * (protected class method to allow for reliable non-data-time-based testing) + * + * @returns number representing the expiry time for this item. + */ + protected calculateExpiry(): number { + return Date.now() + this.expiryDurationMs; + } +} 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 &nbsp; (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 7e4a81acf07e..44a82ee13760 100644 --- a/src/client/common/utils/decorators.ts +++ b/src/client/common/utils/decorators.ts @@ -1,76 +1,216 @@ -import { ProgressLocation, ProgressOptions, window } from 'vscode'; +import '../../common/extensions'; +import { traceError } from '../../logging'; import { isTestExecution } from '../constants'; -// tslint:disable-next-line:no-require-imports no-var-requires +import { createDeferred, Deferred } from './async'; +import { getCacheKeyFromFunctionArgs, getGlobalCacheStore } from './cacheUtils'; +import { StopWatch } from './stopWatch'; + const _debounce = require('lodash/debounce') as typeof import('lodash/debounce'); +type VoidFunction = () => any; +type AsyncVoidFunction = () => Promise<any>; + /** - * Debounces a function execution. Function must return either a void or a promise that resolves to a void. + * Combine multiple sequential calls to the decorated function into one. * @export - * @param {number} [wait] Wait time. + * @param {number} [wait] Wait time (milliseconds). * @returns void + * + * The point is to ensure that successive calls to the function result + * only in a single actual call. Following the most recent call to + * the debounced function, debouncing resets after the "wait" interval + * has elapsed. */ -export function debounce(wait?: number) { - // tslint:disable-next-line:no-any no-function-expression - return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor<any>) { +export function debounceSync(wait?: number) { + if (isTestExecution()) { + // If running tests, lets debounce until the next cycle in the event loop. + // Same as `setTimeout(()=> {}, 0);` with a value of `0`. + wait = undefined; + } + return makeDebounceDecorator(wait); +} + +/** + * Combine multiple sequential calls to the decorated async function into one. + * @export + * @param {number} [wait] Wait time (milliseconds). + * @returns void + * + * The point is to ensure that successive calls to the function result + * only in a single actual call. Following the most recent call to + * the debounced function, debouncing resets after the "wait" interval + * has elapsed. + */ +export function debounceAsync(wait?: number) { + if (isTestExecution()) { + // If running tests, lets debounce until the next cycle in the event loop. + // Same as `setTimeout(()=> {}, 0);` with a value of `0`. + wait = undefined; + } + return makeDebounceAsyncDecorator(wait); +} + +export function makeDebounceDecorator(wait?: number) { + return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor<VoidFunction>) { + // We could also make use of _debounce() options. For instance, + // the following causes the original method to be called + // immediately: + // + // {leading: true, trailing: false} + // + // The default is: + // + // {leading: false, trailing: true} + // + // See https://lodash.com/docs/#debounce. + const options = {}; const originalMethod = descriptor.value!; - // If running tests, lets not debounce (so tests run fast). - wait = wait && isTestExecution() ? undefined : wait; - // tslint:disable-next-line:no-invalid-this no-any - (descriptor as any).value = _debounce(function () { return originalMethod.apply(this, arguments); }, wait); + const debounced = _debounce( + function (this: any) { + return originalMethod.apply(this, arguments as any); + }, + wait, + options, + ); + (descriptor as any).value = debounced; + }; +} + +export function makeDebounceAsyncDecorator(wait?: number) { + return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor<AsyncVoidFunction>) { + type StateInformation = { + started: boolean; + deferred: Deferred<any> | undefined; + timer: NodeJS.Timer | number | undefined; + }; + const originalMethod = descriptor.value!; + const state: StateInformation = { started: false, deferred: undefined, timer: undefined }; + + // Lets defer execution using a setTimeout for the given time. + (descriptor as any).value = function (this: any) { + const existingDeferred: Deferred<any> | undefined = state.deferred; + if (existingDeferred && state.started) { + return existingDeferred.promise; + } + + // Clear previous timer. + const existingDeferredCompleted = existingDeferred && existingDeferred.completed; + const deferred = (state.deferred = + !existingDeferred || existingDeferredCompleted ? createDeferred<any>() : existingDeferred); + if (state.timer) { + clearTimeout(state.timer as any); + } + + state.timer = setTimeout(async () => { + state.started = true; + originalMethod + .apply(this) + .then((r) => { + state.started = false; + deferred.resolve(r); + }) + .catch((ex) => { + state.started = false; + deferred.reject(ex); + }); + }, wait || 0); + return deferred.promise; + }; + }; +} + +type PromiseFunctionWithAnyArgs = (...any: any) => Promise<any>; +const cacheStoreForMethods = getGlobalCacheStore(); + +/** + * 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<PromiseFunctionWithAnyArgs>, + ) { + const originalMethod = descriptor.value!; + const className = 'constructor' in target && target.constructor.name ? target.constructor.name : ''; + const keyPrefix = `Cache_Method_Output_${className}.${propertyName}`; + descriptor.value = async function (...args: any) { + if (isTestExecution()) { + return originalMethod.apply(this, args) as Promise<any>; + } + 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<any>; + } + const cachedItem = cacheStoreForMethods.get(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<any>; + 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; + }; }; } /** * Swallows exceptions thrown by a function. Function must return either a void or a promise that resolves to a void. + * When exceptions (including in promises) are caught, this will return `undefined` to calling code. * @export * @param {string} [scopeName] Scope for the error message to be logged along with the error. * @returns void */ -export function swallowExceptions(scopeName: string) { - // tslint:disable-next-line:no-any no-function-expression +export function swallowExceptions(scopeName?: string) { return function (_target: any, propertyName: string, descriptor: TypedPropertyDescriptor<any>) { 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. if (result && typeof result.then === 'function' && typeof result.catch === 'function') { - return (result as Promise<void>).catch(error => { + return (result as Promise<void>).catch((error) => { if (isTestExecution()) { return; } - console.error(errorMessage, error); + traceError(errorMessage, error); }); } } catch (error) { if (isTestExecution()) { return; } - console.error(errorMessage, error); - } - }; - }; -} - -// tslint:disable-next-line:no-any -type PromiseFunction = (...any: any[]) => Promise<any>; - -export function displayProgress(title: string, location = ProgressLocation.Window) { - return function (_target: Object, _propertyName: string, descriptor: TypedPropertyDescriptor<PromiseFunction>) { - 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); + traceError(errorMessage, error); } - return promise; }; }; } 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 69d248dbf169..78104b48846f 100644 --- a/src/client/common/utils/enum.ts +++ b/src/client/common/utils/enum.ts @@ -3,20 +3,18 @@ 'use strict'; -// tslint:disable:no-any - export function getNamesAndValues<T>(e: any): { name: string; value: T }[] { - return getNames(e).map(n => ({ name: n, value: e[n] })); + return getNames(e).map((n) => ({ name: n, value: e[n] })); } -export function getNames(e: any) { - return getObjValues(e).filter(v => typeof v === 'string') as string[]; +function getNames(e: any) { + return getObjValues(e).filter((v) => typeof v === 'string') as string[]; } export function getValues<T>(e: any) { - return getObjValues(e).filter(v => typeof v === 'number') as any as T[]; + return (getObjValues(e).filter((v) => typeof v === 'number') as any) as T[]; } function getObjValues(e: any): (number | string)[] { - return Object.keys(e).map(k => e[k]); + return Object.keys(e).map((k) => e[k]); } 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<boolean | undefined> { + // 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<FileType | undefined> { + 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<DirEntry | undefined> { + 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<boolean>; + +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<boolean> { + 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/fs.ts b/src/client/common/utils/fs.ts deleted file mode 100644 index 00d9267b5601..000000000000 --- a/src/client/common/utils/fs.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as fs from 'fs'; -import * as path from 'path'; -import * as tmp from 'tmp'; - -export function fsExistsAsync(filePath: string): Promise<boolean> { - return new Promise<boolean>(resolve => { - fs.exists(filePath, exists => { - return resolve(exists); - }); - }); -} -export function fsReaddirAsync(root: string): Promise<string[]> { - return new Promise<string[]>(resolve => { - // Now look for Interpreters in this directory - fs.readdir(root, (err, subDirs) => { - if (err) { - return resolve([]); - } - resolve(subDirs.map(subDir => path.join(root, subDir))); - }); - }); -} - -export function getSubDirectories(rootDir: string): Promise<string[]> { - return new Promise<string[]>(resolve => { - fs.readdir(rootDir, (error, files) => { - if (error) { - return resolve([]); - } - const subDirs: string[] = []; - files.forEach(name => { - const fullPath = path.join(rootDir, name); - try { - if (fs.statSync(fullPath).isDirectory()) { - subDirs.push(fullPath); - } - } - // tslint:disable-next-line:no-empty one-line - catch (ex) { } - }); - resolve(subDirs); - }); - }); -} - -export function createTemporaryFile(extension: string, temporaryDirectory?: string): Promise<{ filePath: string; cleanupCallback: Function }> { - // tslint:disable-next-line:no-any - const options: any = { postfix: extension }; - if (temporaryDirectory) { - options.dir = temporaryDirectory; - } - - return new Promise<{ filePath: string; cleanupCallback: Function }>((resolve, reject) => { - tmp.file(options, (err, tmpFile, fd, cleanupCallback) => { - if (err) { - return reject(err); - } - resolve({ filePath: tmpFile, cleanupCallback: cleanupCallback }); - }); - }); -} diff --git a/src/client/common/utils/icons.ts b/src/client/common/utils/icons.ts new file mode 100644 index 000000000000..71f71898ae9f --- /dev/null +++ b/src/client/common/utils/icons.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { Uri } from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../constants'; + +const darkIconsPath = path.join(EXTENSION_ROOT_DIR, 'resources', 'dark'); +const lightIconsPath = path.join(EXTENSION_ROOT_DIR, 'resources', 'light'); + +export function getIcon(fileName: string): { light: Uri; dark: Uri } { + return { + 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<T = any>(thing: any): thing is Iterable<T> { + 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 985d465c931a..7b7560c74e05 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -1,238 +1,554 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as fs from 'fs'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../constants'; - -// 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 Language Server. Reverting to the alternative, Jedi.'); -} - -export namespace Common { - export const canceled = localize('Common.canceled', 'Canceled'); - export const loadingExtension = localize('Common.loadingPythonExtension', 'Python extension loading...'); -} - -export namespace LanguageService { - export const bannerMessage = localize('LanguageService.bannerMessage', 'Can you please take 2 minutes to tell us how the Python Language Server is working for you?'); - export const bannerLabelYes = localize('LanguageService.bannerLabelYes', 'Yes, take survey now'); - export const bannerLabelNo = localize('LanguageService.bannerLabelNo', 'No, thanks'); -} - -export namespace Interpreters { - export const loading = localize('Interpreters.LoadingInterpreters', 'Loading Python Interpreters'); - export const refreshing = localize('Interpreters.RefreshingInterpreters', 'Refreshing Python Interpreters'); -} - -export namespace Linters { - export const installedButNotEnabled = localize('Linter.InstalledButNotEnabled', 'Linter {0} is installed but not enabled.'); - export const replaceWithSelectedLinter = localize('Linter.replaceWithSelectedLinter', 'Multiple linters are enabled in settings. Replace with \'{0}\'?'); -} - -export namespace DataScienceSurveyBanner { - export const bannerMessage = localize('DataScienceSurveyBanner.bannerMessage', 'Can you please take 2 minutes to tell us how the Python Data Science features are working for you?'); - export const bannerLabelYes = localize('DataScienceSurveyBanner.bannerLabelYes', 'Yes, take survey now'); - export const bannerLabelNo = localize('DataScienceSurveyBanner.bannerLabelNo', 'No, thanks'); -} - -export namespace DataScience { - export const historyTitle = localize('DataScience.historyTitle', 'Python Interactive'); - export const badWebPanelFormatString = localize('DataScience.badWebPanelFormatString', '<html><body><h1>{0} is not a valid file name</h1></body></html>'); - export const sessionDisposed = localize('DataScience.sessionDisposed', 'Cannot execute code, session has been disposed.'); - 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 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 jupyterNotSupported = localize('DataScience.jupyterNotSupported', 'Jupyter is not installed'); - 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 pythonInteractiveHelpLink = localize('DataScience.pythonInteractiveHelpLink', 'See [https://aka.ms/pyaiinstall] for help on installing jupyter.'); - export const importingFormat = localize('DataScience.importingFormat', 'Importing {0}'); - export const startingJupyter = localize('DataScience.startingJupyter', 'Starting Jupyter server'); - 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 importChangeDirectoryComment = localize('DataScience.importChangeDirectoryComment', '#%% Change working directory from the workspace root to the ipynb file location. Turn this addition off with the DataSciece.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 DataSciece.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 restartKernelMessageNo = localize('DataScience.restartKernelMessageNo', 'Cancel'); - export const restartingKernelStatus = localize('DataScience.restartingKernelStatus', 'Restarting iPython Kernel'); - 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 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 clearAll = localize('DataScience.clearAll', 'Remove All Cells'); - export const pythonVersionHeader = localize('DataScience.pythonVersionHeader', 'Python Version:'); - export const pythonRestartHeader = localize('DataScience.pythonRestartHeader', 'Restarted Kernel:'); - export const pythonVersionHeaderNoPyKernel = localize('DataScience.pythonVersionHeaderNoPyKernel', 'Python Version may not match, no ipykernel found:'); - - export const jupyterSelectURILaunchLocal = localize('DataScience.jupyterSelectURILaunchLocal', 'Launch local Jupyter server'); - export const jupyterSelectURISpecifyURI = localize('DataScience.jupyterSelectURISpecifyURI', 'Type in the URI for the Jupyter server'); - export const jupyterSelectURIPrompt = localize('DataScience.jupyterSelectURIPrompt', 'Enter the URI of a Jupyter server'); - export const jupyterSelectURIInvalidURI = localize('DataScience.jupyterSelectURIInvalidURI', 'Invalid URI specified'); - 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 notebookVersionFormat = localize('DataScience.notebookVersionFormat', 'Jupyter Notebook Version: {0}'); - //tslint:disable-next-line:no-multiline-string - export const jupyterKernelNotSupportedOnActive = localize('DataScience.jupyterKernelNotSupportedOnActive', `iPython kernel cannot be started from '{0}'. Using closest match {1} instead.`); - export const jupyterKernelSpecNotFound = localize('DataScience.jupyterKernelSpecNotFound', 'Cannot create a iPython kernel spec and none are available for use'); - export const interruptKernel = localize('DataScience.interruptKernel', 'Interrupt iPython Kernel'); - export const interruptKernelStatus = localize('DataScience.interruptKernelStatus', 'Interrupting iPython Kernel'); - export const exportCancel = localize('DataScience.exportCancel', 'Cancel'); - 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 namespace DebugConfigurationPrompts { - export const selectConfigurationTitle = localize('debug.selectConfigurationTitle', 'Select a debug configuration'); - export const selectConfigurationPlaceholder = localize('debug.selectConfigurationPlaceholder', 'Debug Configuration'); - export const debugFileConfigurationLabel = localize('debug.debugFileConfigurationLabel', 'Python File'); - export const debugFileConfigurationDescription = localize('debug.debugFileConfigurationDescription', 'Debug currently active Python file'); - export const debugModuleConfigurationLabel = localize('debug.debugModuleConfigurationLabel', 'Module'); - export const debugModuleConfigurationDescription = localize('debug.debugModuleConfigurationDescription', 'Debug a python module by invoking it with \'-m\''); - export const remoteAttachConfigurationLabel = localize('debug.remoteAttachConfigurationLabel', 'Remote Attach'); - export const remoteAttachConfigurationDescription = localize('debug.remoteAttachConfigurationDescription', 'Attach to a remote ptsvd debug server'); - export const debugDjangoConfigurationLabel = localize('debug.debugDjangoConfigurationLabel', 'Django'); - export const debugDjangoConfigurationDescription = localize('debug.debugDjangoConfigurationDescription', 'Launch and debug a Django web application'); - export const debugFlaskConfigurationLabel = localize('debug.debugFlaskConfigurationLabel', 'Flask'); - export const debugFlaskConfigurationDescription = localize('debug.debugFlaskConfigurationDescription', 'Launch and debug a Flask web application'); - export const debugPyramidConfigurationLabel = localize('debug.debugPyramidConfigurationLabel', 'Pyramid'); - export const debugPyramidConfigurationDescription = localize('debug.debugPyramidConfigurationDescription', 'Web Application'); - export const djangoEnterManagePyPathTitle = localize('debug.djangoEnterManagePyPathTitle', 'Debug Django'); - // tslint:disable-next-line:no-invalid-template-strings - export const djangoEnterManagePyPathPrompt = localize('debug.djangoEnterManagePyPathPrompt', 'Enter path to manage.py (\'${workspaceFolder}\' points to the root of the current workspace folder)'); - export const djangoEnterManagePyPathInvalidFilePathError = localize('debug.djangoEnterManagePyPathInvalidFilePathError', 'Enter a valid python file path'); - export const flaskEnterAppPathOrNamePathTitle = localize('debug.flaskEnterAppPathOrNamePathTitle', 'Debug Flask'); - export const flaskEnterAppPathOrNamePathPrompt = localize('debug.flaskEnterAppPathOrNamePathPrompt', 'Enter path to application, e.g. \'app.py\' or \'app\''); - export const flaskEnterAppPathOrNamePathInvalidNameError = localize('debug.flaskEnterAppPathOrNamePathInvalidNameError', 'Enter a valid name'); - - export const moduleEnterModuleTitle = localize('debug.moduleEnterModuleTitle', 'Debug Module'); - export const moduleEnterModulePrompt = localize('debug.moduleEnterModulePrompt', 'Enter Python module/package name'); - export const moduleEnterModuleInvalidNameError = localize('debug.moduleEnterModuleInvalidNameError', 'Enter a valid module name'); - export const pyramidEnterDevelopmentIniPathTitle = localize('debug.pyramidEnterDevelopmentIniPathTitle', 'Debug Pyramid'); - // tslint:disable-next-line:no-invalid-template-strings - export const pyramidEnterDevelopmentIniPathPrompt = localize('debug.pyramidEnterDevelopmentIniPathPrompt', '`Enter path to development.ini (\'${workspaceFolderToken}\' points to the root of the current workspace folder)`'); - export const pyramidEnterDevelopmentIniPathInvalidFilePathError = localize('debug.pyramidEnterDevelopmentIniPathInvalidFilePathError', 'Enter a valid file path'); - export const attachRemotePortTitle = localize('debug.attachRemotePortTitle', 'Remote Debugging'); - export const attachRemotePortPrompt = localize('debug.attachRemotePortPrompt', 'Enter the port number that the ptvsd server is listening on'); - export const attachRemotePortValidationError = localize('debug.attachRemotePortValidationError', 'Enter a valid port number'); - export const attachRemoteHostTitle = localize('debug.attachRemoteHostTitle', 'Remote Debugging'); - export const attachRemoteHostPrompt = localize('debug.attachRemoteHostPrompt', 'Enter host name'); - export const attachRemoteHostValidationError = localize('debug.attachRemoteHostValidationError', 'Enter a host name or IP address'); -} - -export namespace UnitTests { - export const testErrorDiagnosticMessage = localize('UnitTests.testErrorDiagnosticMessage', 'Error'); - export const testFailDiagnosticMessage = localize('UnitTests.testFailDiagnosticMessage', 'Fail'); - export const testSkippedDiagnosticMessage = localize('UnitTests.testSkippedDiagnosticMessage', 'Skipped'); -} - -// Skip using vscode-nls and instead just compute our strings based on key values. Key values -// can be loaded out of the nls.<locale>.json files -let loadedCollection: { [index: string]: string } | undefined; -let defaultCollection: { [index: string]: string } | undefined; -const askedForCollection: { [index: string]: string } = {}; -let loadedLocale: string; - -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 function getCollection() { - // Load the current collection - if (!loadedCollection || parseLocale() !== loadedLocale) { - load(); - } - - // Combine the default and loaded collections - return { ...defaultCollection, ...loadedCollection }; -} - -export function getAskedForCollection() { - return askedForCollection; -} - -function parseLocale(): string { - // Attempt to load from the vscode locale. If not there, use english - const vscodeConfigString = process.env.VSCODE_NLS_CONFIG; - return vscodeConfigString ? JSON.parse(vscodeConfigString).locale : 'en-us'; -} - -function getString(key: string, defValue: string) { - // Load the current collection - if (!loadedCollection || parseLocale() !== loadedLocale) { - load(); - } - - // First lookup in the dictionary that matches the current locale - if (loadedCollection && loadedCollection.hasOwnProperty(key)) { - askedForCollection[key] = loadedCollection[key]; - return loadedCollection[key]; - } - - // Fallback to the default dictionary - if (defaultCollection && defaultCollection.hasOwnProperty(key)) { - askedForCollection[key] = defaultCollection[key]; - return defaultCollection[key]; - } - - // Not found, return the default - askedForCollection[key] = defValue; - return defValue; -} - -function load() { - // 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.existsSync(nlsFile)) { - const contents = fs.readFileSync(nlsFile, 'utf8'); - loadedCollection = JSON.parse(contents); - } else { - // If there isn't one, at least remember that we looked so we don't try to load a second time - loadedCollection = {}; - } - - // Get the default collection if necessary. Strings may be in the default or the locale json - if (!defaultCollection) { - const defaultNlsFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json'); - if (fs.existsSync(defaultNlsFile)) { - const contents = fs.readFileSync(defaultNlsFile, 'utf8'); - defaultCollection = JSON.parse(contents); - } else { - defaultCollection = {}; - } - } -} - -// Default to loading the current locale -load(); +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +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 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 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 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 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 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 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 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 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 namespace Common { + 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 = 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 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 remindMeLater = l10n.t('Remind me later'); + + 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 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 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 useCurrentWorkingDirectoryDetail = l10n.t( + 'TensorBoard will search for tfevent files in all subdirectories of the current working directory', + ); + 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 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 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 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 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 missingSourceFile = l10n.t( + 'The Python extension could not locate the requested source file on disk. Please manually specify the file.', + ); + 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 LanguageService { + export const virtualWorkspaceStatusItem = { + detail: l10n.t('Limited IntelliSense supported by Jedi and Pylance'), + }; + export const statusItem = { + name: l10n.t('Python IntelliSense Status'), + text: l10n.t('Partial Mode'), + detail: l10n.t('Limited IntelliSense provided by Pylance'), + }; + 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 reloadAfterLanguageServerChange = l10n.t( + 'Reload the window after switching between language servers.', + ); + + 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 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 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 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 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 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 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 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 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 envExtDiscoveryNoEnvironments = l10n.t( + 'Environment discovery completed but no Python environments were found. Check the "Python Environments" output channel for details.', + ); + 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 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 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 shellIntegrationEnvVarCollectionDescription = l10n.t( + 'Enables `python.terminal.shellIntegration.enabled` by modifying `PYTHONSTARTUP` and `PYTHON_BASIC_REPL`', + ); + export const shellIntegrationDisabledEnvVarCollectionDescription = l10n.t( + 'Disables `python.terminal.shellIntegration.enabled` by unsetting `PYTHONSTARTUP` and `PYTHON_BASIC_REPL`', + ); + 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 activatedCondaEnvLaunch = l10n.t( + 'We noticed VS Code was launched from an activated conda environment, would you like to select it?', + ); + 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 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 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 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 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 namespace OutputChannelNames { + export const languageServer = l10n.t('Python Language Server'); + export const python = l10n.t('Python'); +} + +export namespace Linters { + export const selectLinter = l10n.t('Select Linter'); +} + +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 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: l10n.t('Select a debug configuration'), + placeholder: l10n.t('Debug Configuration'), + }; + export const launchJsonCompletions = { + label: l10n.t('Python'), + description: l10n.t('Select a Python debug configuration'), + }; + + export namespace file { + export const snippet = { + name: l10n.t('Python: Current File'), + }; + + export const selectConfiguration = { + label: l10n.t('Python File'), + description: l10n.t('Debug the currently active Python file'), + }; + } + export namespace module { + export const snippet = { + name: l10n.t('Python: Module'), + default: l10n.t('enter-your-module-name'), + }; + + export const selectConfiguration = { + label: l10n.t('Module'), + description: l10n.t("Debug a Python module by invoking it with '-m'"), + }; + export const enterModule = { + 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: l10n.t('Python: Remote Attach'), + }; + + export const selectConfiguration = { + label: l10n.t('Remote Attach'), + description: l10n.t('Attach to a remote debug server'), + }; + export const enterRemoteHost = { + 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: 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: l10n.t('Python: Attach using Process Id'), + }; + + export const selectConfiguration = { + label: l10n.t('Attach using Process ID'), + description: l10n.t('Attach to a local process'), + }; + } + export namespace django { + export const snippet = { + name: l10n.t('Python: Django'), + }; + + export const selectConfiguration = { + label: l10n.t('Django'), + description: l10n.t('Launch and debug a Django web application'), + }; + export const enterManagePyPath = { + 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: l10n.t('Python: Flask'), + }; + + export const selectConfiguration = { + label: l10n.t('Flask'), + description: l10n.t('Launch and debug a Flask web application'), + }; + export const enterAppPathOrNamePath = { + title: l10n.t('Debug Flask'), + prompt: l10n.t('Python: Flask'), + invalid: l10n.t('Enter a valid name'), + }; + } + export namespace pyramid { + export const snippet = { + name: l10n.t('Python: Pyramid Application'), + }; + + export const selectConfiguration = { + label: l10n.t('Pyramid'), + description: l10n.t('Launch and debug a Pyramid web application'), + }; + export const enterDevelopmentIniPath = { + title: l10n.t('Debug Pyramid'), + invalid: l10n.t('Enter a valid file path'), + }; + } +} + +export namespace Testing { + 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 = 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.', + ); +} + +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).', + ); +} + +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).", + ); +} + +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...'); + + export const hasVirtualEnv = l10n.t('Workspace folder contains a virtual environment'); + + export const noWorkspace = l10n.t('A workspace is required when creating an environment using venv.'); + + export const pickWorkspacePlaceholder = l10n.t('Select a workspace to create environment'); + + export const providersQuickPickPlaceholder = l10n.t('Select an environment type'); + + 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'); + } + + 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.'); + } + + 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?', + ); + } +} + +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 3b91f048ff43..a461d25d9d30 100644 --- a/src/client/common/utils/misc.ts +++ b/src/client/common/utils/misc.ts @@ -1,7 +1,99 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - 'use strict'; +import type { TextDocument, Uri } from 'vscode'; +import { InteractiveInputScheme, NotebookCellScheme } from '../constants'; +import { InterpreterUri } from '../installer/types'; +import { isParentPath } from '../platform/fs-paths'; +import { Resource } from '../types'; + +export function noop() {} + +/** + * Like `Readonly<>`, but recursive. + * + * See https://github.com/Microsoft/TypeScript/pull/21316. + */ + +type DeepReadonly<T> = T extends any[] ? IDeepReadonlyArray<T[number]> : DeepReadonlyNonArray<T>; +type DeepReadonlyNonArray<T> = T extends object ? DeepReadonlyObject<T> : T; +interface IDeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {} +type DeepReadonlyObject<T> = { + readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>; +}; +type NonFunctionPropertyNames<T> = { [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 function isResource(resource?: InterpreterUri): resource is Resource { + if (!resource) { + return true; + } + const uri = resource as Uri; + return typeof uri.path === 'string' && typeof uri.scheme === 'string'; +} + +/** + * Checking whether something is a Uri. + * Using `instanceof Uri` doesn't always work as the object is not an instance of Uri (at least not in tests). + * That's why VSC too has a helper method `URI.isUri` (though not public). + */ + +function isUri(resource?: Uri | any): resource is Uri { + if (!resource) { + return false; + } + const uri = resource as Uri; + return typeof uri.path === 'string' && typeof uri.scheme === 'string'; +} + +/** + * 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; +} -// tslint:disable-next-line:no-empty -export function noop() { } +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 90e3a6192a8a..2de1684a4d2e 100644 --- a/src/client/common/utils/multiStepInput.ts +++ b/src/client/common/utils/multiStepInput.ts @@ -1,111 +1,226 @@ // 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<T extends any> = (input: MultiStepInput<T>, state: T) => Promise<InputStep<T> | void>; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type InputStep<T> = (input: MultiStepInput<T>, state: T) => Promise<InputStep<T> | void>; -export interface IQuickPickParameters<T extends QuickPickItem> { - title: string; +type buttonCallbackType<T extends QuickPickItem> = (quickPick: QuickPick<T>) => void; + +export type QuickInputButtonSetup = { + /** + * Button for an action in a QuickPick. + */ + button: QuickInputButton; + /** + * Callback to be invoked when button is clicked. + */ + callback: buttonCallbackType<QuickPickItem>; +}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface IQuickPickParameters<T extends QuickPickItem, E = any> { + title?: string; step?: number; totalSteps?: number; canGoBack?: boolean; items: T[]; - activeItem?: T; - placeholder: string; - buttons?: QuickInputButton[]; - shouldResume?(): Promise<boolean>; + activeItem?: T | ((quickPick: QuickPick<T>) => Promise<T>); + placeholder: string | undefined; + customButtonSetups?: QuickInputButtonSetup[]; + matchOnDescription?: boolean; + matchOnDetail?: boolean; + keepScrollPosition?: boolean; + sortByLabel?: boolean; + acceptFilterBoxTextAsSelection?: boolean; + /** + * A method called only after quickpick has been created and all handlers are registered. + */ + initialize?: (quickPick: QuickPick<T>) => void; + onChangeItem?: { + callback: (event: E, quickPick: QuickPick<T>) => void; + event: Event<E>; + }; } -export interface InputBoxParameters { +interface InputBoxParameters { title: string; + password?: boolean; step?: number; totalSteps?: number; value: string; prompt: string; buttons?: QuickInputButton[]; validate(value: string): Promise<string | undefined>; - shouldResume?(): Promise<boolean>; } -type MultiStepInputQuickPicResponseType<T, P> = T | (P extends { buttons: (infer I)[] } ? I : never); -type MultiStepInputInputBoxResponseType<P> = string | (P extends { buttons: (infer I)[] } ? I : never); +type MultiStepInputQuickPickResponseType<T, P> = T | (P extends { buttons: (infer I)[] } ? I : never) | undefined; +type MultiStepInputInputBoxResponseType<P> = string | (P extends { buttons: (infer I)[] } ? I : never) | undefined; export interface IMultiStepInput<S> { run(start: InputStep<S>, state: S): Promise<void>; - showQuickPick<T extends QuickPickItem, P extends IQuickPickParameters<T>>({ title, step, totalSteps, items, activeItem, placeholder, buttons, shouldResume }: P): Promise<MultiStepInputQuickPicResponseType<T, P>>; - showInputBox<P extends InputBoxParameters>({ title, step, totalSteps, value, prompt, validate, buttons, shouldResume }: P): Promise<MultiStepInputInputBoxResponseType<P>>; + showQuickPick<T extends QuickPickItem, P extends IQuickPickParameters<T>>({ + title, + step, + totalSteps, + items, + activeItem, + placeholder, + customButtonSetups, + }: P): Promise<MultiStepInputQuickPickResponseType<T, P>>; + showInputBox<P extends InputBoxParameters>({ + title, + step, + totalSteps, + value, + prompt, + validate, + buttons, + }: P): Promise<MultiStepInputInputBoxResponseType<P>>; } export class MultiStepInput<S> implements IMultiStepInput<S> { private current?: QuickInput; + private steps: InputStep<S>[] = []; - constructor(private readonly shell: IApplicationShell) { } - public run(start: InputStep<S>, state: S) { + + constructor(private readonly shell: IApplicationShell) {} + + public run(start: InputStep<S>, state: S): Promise<void> { return this.stepThrough(start, state); } - public async showQuickPick<T extends QuickPickItem, P extends IQuickPickParameters<T>>({ title, step, totalSteps, items, activeItem, placeholder, buttons, shouldResume }: P): Promise<MultiStepInputQuickPicResponseType<T, P>> { + public async showQuickPick<T extends QuickPickItem, P extends IQuickPickParameters<T>>({ + title, + step, + totalSteps, + items, + activeItem, + placeholder, + customButtonSetups, + matchOnDescription, + matchOnDetail, + acceptFilterBoxTextAsSelection, + onChangeItem, + keepScrollPosition, + sortByLabel, + initialize, + }: P): Promise<MultiStepInputQuickPickResponseType<T, P>> { const disposables: Disposable[] = []; - try { - return await new Promise<MultiStepInputQuickPicResponseType<T, P>>((resolve, reject) => { - const input = this.shell.createQuickPick<T>(); - input.title = title; - input.step = step; - input.totalSteps = totalSteps; - input.placeholder = placeholder; - input.ignoreFocusOut = true; - input.items = items; - if (activeItem) { - input.activeItems = [activeItem]; + const input = this.shell.createQuickPick<T>(); + 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<T>(); + + 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(<any>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 (this.current) { - this.current.dispose(); + } } - this.current = input; - this.current.show(); - }); + }), + input.onDidChangeSelection((selectedItems) => deferred.resolve(selectedItems[0])), + input.onDidHide(() => { + if (!deferred.completed) { + deferred.resolve(undefined); + } + }), + ); + 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()); + disposables.forEach((d) => d.dispose()); } } - public async showInputBox<P extends InputBoxParameters>({ title, step, totalSteps, value, prompt, validate, buttons, shouldResume }: P): Promise<MultiStepInputInputBoxResponseType<P>> { + public async showInputBox<P extends InputBoxParameters>({ + title, + step, + totalSteps, + value, + prompt, + validate, + password, + buttons, + }: P): Promise<MultiStepInputInputBoxResponseType<P>> { const disposables: Disposable[] = []; try { return await new Promise<MultiStepInputInputBoxResponseType<P>>((resolve, reject) => { @@ -113,20 +228,19 @@ export class MultiStepInput<S> implements IMultiStepInput<S> { input.title = title; input.step = step; input.totalSteps = totalSteps; + input.password = !!password; input.value = value || ''; input.prompt = prompt; input.ignoreFocusOut = true; - input.buttons = [ - ...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), - ...(buttons || []) - ]; + input.buttons = [...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), ...(buttons || [])]; let validating = validate(''); disposables.push( - input.onDidTriggerButton(item => { + input.onDidTriggerButton((item) => { if (item === QuickInputButtons.Back) { reject(InputFlowAction.back); } else { - resolve(<any>item); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolve(item as any); } }), input.onDidAccept(async () => { @@ -139,7 +253,7 @@ export class MultiStepInput<S> implements IMultiStepInput<S> { input.enabled = true; input.busy = false; }), - input.onDidChangeValue(async text => { + input.onDidChangeValue(async (text) => { const current = validate(text); validating = current; const validationMessage = await current; @@ -148,11 +262,8 @@ export class MultiStepInput<S> implements IMultiStepInput<S> { } }), input.onDidHide(() => { - (async () => { - reject(shouldResume && await shouldResume() ? InputFlowAction.resume : InputFlowAction.cancel); - })() - .catch(reject); - }) + resolve(undefined); + }), ); if (this.current) { this.current.dispose(); @@ -161,11 +272,11 @@ export class MultiStepInput<S> implements IMultiStepInput<S> { this.current.show(); }); } finally { - disposables.forEach(d => d.dispose()); + disposables.forEach((d) => d.dispose()); } } - private async stepThrough<T>(start: InputStep<S>, state: S) { + private async stepThrough(start: InputStep<S>, state: S) { let step: InputStep<S> | void = start; while (step) { this.steps.push(step); @@ -179,6 +290,9 @@ export class MultiStepInput<S> implements IMultiStepInput<S> { 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) { @@ -199,7 +313,8 @@ export interface IMultiStepInputFactory { } @injectable() export class MultiStepInputFactory { - constructor(@inject(IApplicationShell) private readonly shell: IApplicationShell) { } + constructor(@inject(IApplicationShell) private readonly shell: IApplicationShell) {} + public create<S>(): IMultiStepInput<S> { return new MultiStepInput<S>(this.shell); } diff --git a/src/client/common/utils/platform.ts b/src/client/common/utils/platform.ts index e293819b364f..a1a49ba3c427 100644 --- a/src/client/common/utils/platform.ts +++ b/src/client/common/utils/platform.ts @@ -3,14 +3,79 @@ '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. +export function getOSType(platform: string = process.platform): OSType { + if (/^win/.test(platform)) { + return OSType.Windows; + } else if (/^darwin/.test(platform)) { + return OSType.OSX; + } else if (/^linux/.test(platform)) { + return OSType.Linux; + } else { + return OSType.Unknown; + } +} + +const architectures: Record<string, Architecture> = { + 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 024e380b426d..a766df771116 100644 --- a/src/client/common/utils/random.ts +++ b/src/client/common/utils/random.ts @@ -14,12 +14,12 @@ function getRandom(): number { num = (buf.readUInt8(0) << 8) + buf.readUInt8(1); const maxValue: number = Math.pow(16, 4) - 1; - return (num / maxValue); + 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)); + return min + randomVal * (max - min); } @injectable() diff --git a/src/client/common/utils/regexp.ts b/src/client/common/utils/regexp.ts new file mode 100644 index 000000000000..d05d7fc60204 --- /dev/null +++ b/src/client/common/utils/regexp.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +/* Generate a RegExp from a "verbose" pattern. + * + * All whitespace in the pattern is removed, including newlines. This + * allows the pattern to be much more readable by allowing it to span + * multiple lines and to separate tokens with insignificant whitespace. + * The functionality is similar to the VERBOSE ("x") flag in Python's + * regular expressions. + * + * Note that significant whitespace in the pattern must be explicitly + * indicated by "\s". Also, unlike with regular expression literals, + * backslashes must be escaped. Conversely, forward slashes do not + * need to be escaped. + * + * Line comments are also removed. A comment is two spaces followed + * by `#` followed by a space and then the rest of the text to the + * end of the line. + */ +export function verboseRegExp(pattern: string, flags?: string): RegExp { + pattern = pattern.replace(/(^| {2})# .*$/gm, ''); + pattern = pattern.replace(/\s+?/g, ''); + return RegExp(pattern, flags); +} diff --git a/src/client/common/utils/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<T extends IDisposable>(disposable: T): T; +export function dispose<T extends IDisposable>(disposable: T | undefined): T | undefined; +export function dispose<T extends IDisposable, A extends Iterable<T> = Iterable<T>>(disposables: A): A; +export function dispose<T extends IDisposable>(disposables: Array<T>): Array<T>; +export function dispose<T extends IDisposable>(disposables: ReadonlyArray<T>): ReadonlyArray<T>; +// eslint-disable-next-line @typescript-eslint/no-explicit-any, consistent-return +export function dispose<T extends IDisposable>(arg: T | Iterable<T> | 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<void> { + 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<void> { + 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<IDisposable>(); + + 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<T extends IDisposable>(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<T extends IDisposable>(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/stopWatch.ts b/src/client/common/utils/stopWatch.ts index a72b3f3ae349..9c9a73d8279e 100644 --- a/src/client/common/utils/stopWatch.ts +++ b/src/client/common/utils/stopWatch.ts @@ -3,12 +3,16 @@ 'use strict'; -export class StopWatch { - private started: number = Date.now(); +export class StopWatch implements IStopWatch { + private started = new Date().getTime(); public get elapsedTime() { - return Date.now() - this.started; + return new Date().getTime() - this.started; } - public reset(){ - this.started = Date.now(); + public reset() { + this.started = new Date().getTime(); } } + +export interface IStopWatch { + elapsedTime: number; +} diff --git a/src/client/common/utils/string.ts b/src/client/common/utils/string.ts deleted file mode 100644 index 00f2e014253e..000000000000 --- a/src/client/common/utils/string.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -/** - * 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/client/common/utils/sysTypes.ts b/src/client/common/utils/sysTypes.ts index 6045b6beeaa8..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', }; /** @@ -23,7 +21,7 @@ export function isArray(array: any): array is any[] { return Array.isArray(array); } - if (array && typeof (array.length) === _typeof.number && array.constructor === Array) { + if (array && typeof array.length === _typeof.number && array.constructor === Array) { return true; } @@ -34,31 +32,26 @@ export function isArray(array: any): array is any[] { * @returns whether the provided parameter is a JavaScript String or not. */ export function isString(str: any): str is string { - if (typeof (str) === _typeof.string || str instanceof String) { + if (typeof str === _typeof.string || str instanceof String) { return true; } return false; } -/** - * @returns whether the provided parameter is a JavaScript Array and each element in the array is a string. - */ -export function isStringArray(value: any): value is string[] { - return isArray(value) && (<any[]>value).every(elem => isString(elem)); -} - /** * * @returns whether the provided parameter is of type `object` but **not** * `null`, an `array`, a `regexp`, nor a `date`. */ export function isObject(obj: any): obj is any { - return typeof obj === _typeof.object - && obj !== null - && !Array.isArray(obj) - && !(obj instanceof RegExp) - && !(obj instanceof Date); + return ( + typeof obj === _typeof.object && + obj !== null && + !Array.isArray(obj) && + !(obj instanceof RegExp) && + !(obj instanceof Date) + ); } /** @@ -66,63 +59,9 @@ export function isObject(obj: any): obj is any { * @returns whether the provided parameter is a JavaScript Number or not. */ export function isNumber(obj: any): obj is number { - if ((typeof (obj) === _typeof.number || obj instanceof Number) && !isNaN(obj)) { + if ((typeof obj === _typeof.number || obj instanceof Number) && !isNaN(obj)) { return true; } return false; } - -/** - * @returns whether the provided parameter is a JavaScript Boolean or not. - */ -export function isBoolean(obj: any): obj is boolean { - return obj === true || obj === false; -} - -/** - * @returns whether the provided parameter is undefined. - */ -export function isUndefined(obj: any): boolean { - return typeof (obj) === _typeof.undefined; -} - -/** - * @returns whether the provided parameter is undefined or null. - */ -export function isUndefinedOrNull(obj: any): boolean { - return isUndefined(obj) || obj === null; -} - -const hasOwnProperty = Object.prototype.hasOwnProperty; - -/** - * @returns whether the provided parameter is an empty JavaScript Object or not. - */ -export function isEmptyObject(obj: any): obj is any { - if (!isObject(obj)) { - return false; - } - - for (let key in obj) { - if (hasOwnProperty.call(obj, key)) { - return false; - } - } - - return true; -} - -/** - * @returns whether the provided parameter is a JavaScript Function or not. - */ -export function isFunction(obj: any): obj is Function { - return typeof obj === _typeof.function; -} - -/** - * @returns whether the provided parameters is are JavaScript Function or not. - */ -export function areFunctions(...objects: any[]): boolean { - return objects && objects.length > 0 && objects.every(isFunction); -} diff --git a/src/client/common/utils/text.ts b/src/client/common/utils/text.ts 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 a631efa52248..b3d9ed3d2f46 100644 --- a/src/client/common/utils/version.ts +++ b/src/client/common/utils/version.ts @@ -4,44 +4,404 @@ 'use strict'; import * as semver from 'semver'; -import { Version } from '../types'; +import { verboseRegExp } from './regexp'; -export function parseVersion(raw: string): semver.SemVer { - raw = raw.replace(/\.00*(?=[1-9]|0\.)/, '.'); - const ver = semver.coerce(raw); - if (ver === null || !semver.valid(ver)) { - // tslint:disable-next-line: no-suspicious-comment - // TODO: Raise an exception instead? - return new semver.SemVer('0.0.0'); +// 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, '']; } - return ver; + 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<T extends BasicVersionInfo>(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<T extends BasicVersionInfo>(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; } -export function parsePythonVersion(version: string): Version | undefined { - if (!version || version.trim().length === 0) { + +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; } - const versionParts = (version || '') - .split('.') - .map(item => item.trim()) - .filter(item => item.length > 0) - .filter((_, index) => index < 4); + throw Error(`invalid ${prop} version (failed to normalize; ${unnormalized})`); +} - 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()); +/** + * 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<T extends BasicVersionInfo>(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'); } - while (versionParts.length < 4) { - versionParts.push(''); + if (info.minor < 0) { + if (info.micro === 0 || info.micro > 0) { + throw Error('missing minor version'); + } } - // 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'; +} + +/** + * Convert the info to a simple string. + * + * Any negative parts are ignored. + * + * The object is expected to be normalized. + */ +export function getVersionString<T extends BasicVersionInfo>(info: T): string { + if (info.major < 0) { + return ''; } - if (['alpha', 'beta', 'candidate', 'final'].indexOf(versionParts[3]) === -1) { - versionParts.pop(); + if (info.minor < 0) { + return `${info.major}`; } - const numberParts = `${versionParts[0]}.${versionParts[1]}.${versionParts[2]}`; - const rawVersion = versionParts.length === 4 ? `${numberParts}-${versionParts[3]}` : numberParts; - return new semver.SemVer(rawVersion); + if (info.micro < 0) { + return `${info.major}.${info.minor}`; + } + return `${info.major}.${info.minor}.${info.micro}`; +} + +export type ParseResult<T extends BasicVersionInfo = BasicVersionInfo> = { + version: T; + before: string; + after: string; +}; + +const basicVersionPattern = ` + ^ + (.*?) # <before> + (\\d+) # <major> + (?: + [.] + (\\d+) # <minor> + (?: + [.] + (\\d+) # <micro> + )? + )? + ([^\\d].*)? # <after> + $ +`; +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<T extends BasicVersionInfo>(verStr: string): ParseResult<T> | 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<T extends BasicVersionInfo>(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<T extends BasicVersionInfo, V extends BasicVersionInfo>( + // 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<T extends VersionInfo>(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<T extends VersionInfo>(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<T extends VersionInfo>(verStr: string): ParseResult<T> | undefined { + const result = parseBasicVersionInfo<T>(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<T extends BasicVersionInfo, V extends BasicVersionInfo>( + // 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<T extends BasicVersionInfo, V extends BasicVersionInfo>( + // 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)) { + // TODO: Raise an exception instead? + return new semver.SemVer('0.0.0'); + } + return ver; } diff --git a/src/client/common/utils/workerPool.ts b/src/client/common/utils/workerPool.ts new file mode 100644 index 000000000000..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<T> = () => Promise<T>; +type WorkFunc<T, R> = (item: T) => Promise<R>; +type PostResult<T, R> = (item: T, result?: R, err?: Error) => void; + +interface IWorkItem<T> { + item: T; +} + +export enum QueuePosition { + Back, + Front, +} + +export interface IWorkerPool<T, R> 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<R>; +} + +class Worker<T, R> implements IWorker { + private stopProcessing: boolean = false; + public constructor( + private readonly next: NextFunc<T>, + private readonly workFunc: WorkFunc<T, R>, + private readonly postResult: PostResult<T, R>, + 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<T, R> { + private readonly items: IWorkItem<T>[] = []; + private readonly results: Map<IWorkItem<T>, Deferred<R>> = new Map(); + public add(item: T, position?: QueuePosition): Promise<R> { + // 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<T> = { 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<R>(); + this.results.set(workItem, deferred); + + return deferred.promise; + } + + public completed(workItem: IWorkItem<T>, 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<T> | undefined { + return this.items.shift(); + } + + public clear(): void { + this.results.forEach((v: Deferred<R>, k: IWorkItem<T>, map: Map<IWorkItem<T>, Deferred<R>>) => { + v.reject(Error('Queue stopped processing')); + map.delete(k); + }); + } +} + +class WorkerPool<T, R> implements IWorkerPool<T, R> { + // 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<T>): void; stop(): void }[] = []; + + // A collection that manages the work items. + private readonly queue = new WorkQueue<T, R>(); + + // State of the pool manages via stop(), start() + private stopProcessing = false; + + public constructor( + private readonly workerFunc: WorkFunc<T, R>, + private readonly numWorkers: number = 2, + private readonly name: string = 'Worker', + ) {} + + public addToQueue(item: T, position?: QueuePosition): Promise<R> { + 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<IWorkItem<T>, R>( + () => this.nextWorkItem(), + (workItem: IWorkItem<T>) => this.workerFunc(workItem.item), + (workItem: IWorkItem<T>, 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<IWorkItem<T>> { + // 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<IWorkItem<T>>((resolve, reject) => { + this.waitingWorkersUnblockQueue.push({ + unblock: (workItem: IWorkItem<T>) => { + // 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<T, R>( + workerFunc: WorkFunc<T, R>, + numWorkers?: number, + name?: string, +): WorkerPool<T, R> { + const pool = new WorkerPool<T, R>(workerFunc, numWorkers, name); + pool.start(); + return pool; +} diff --git a/src/client/common/variables/environment.ts b/src/client/common/variables/environment.ts index dd1146f98f04..9f0abd9b0ee7 100644 --- a/src/client/common/variables/environment.ts +++ b/src/client/common/variables/environment.ts @@ -1,52 +1,112 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import * as dotenv from 'dotenv'; -import * as fs from 'fs-extra'; +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 { - private readonly pathVariable: 'PATH' | 'Path'; - constructor(@inject(IPathUtils) pathUtils: IPathUtils) { - this.pathVariable = pathUtils.getPathVariableName(); + private _pathVariable?: 'Path' | 'PATH'; + constructor( + // We only use a small portion of either of these interfaces. + @inject(IPathUtils) private readonly pathUtils: IPathUtils, + @inject(IFileSystem) private readonly fs: IFileSystem, + ) {} + + public async parseFile( + filePath?: string, + baseVars?: EnvironmentVariables, + ): Promise<EnvironmentVariables | undefined> { + if (!filePath || !(await this.fs.pathExists(filePath))) { + return; + } + 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 async parseFile(filePath?: string): Promise<EnvironmentVariables | undefined> { - if (!filePath || !await fs.pathExists(filePath)) { + + public parseFileSync(filePath?: string, baseVars?: EnvironmentVariables): EnvironmentVariables | undefined { + if (!filePath || !pathExistsSync(filePath)) { return; } - if (!fs.lstatSync(filePath).isFile()) { + 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 dotenv.parse(await fs.readFile(filePath)); + return parseEnvFile(contents, baseVars); } - public mergeVariables(source: EnvironmentVariables, target: EnvironmentVariables) { + + 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) { + Object.keys(source).forEach((setting) => { + 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[]) { return this.appendPaths(vars, 'PYTHONPATH', ...pythonPaths); } + public appendPath(vars: EnvironmentVariables, ...paths: string[]) { return this.appendPaths(vars, this.pathVariable, ...paths); } - private appendPaths(vars: EnvironmentVariables, variableName: 'PATH' | 'Path' | 'PYTHONPATH', ...pathsToAppend: string[]) { + + private get pathVariable(): string { + if (!this._pathVariable) { + this._pathVariable = this.pathUtils.getPathVariableName(); + } + return normCase(this._pathVariable)!; + } + + 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()) + .filter((item) => typeof item === 'string' && item.trim().length > 0) + .map((item) => item.trim()) .join(path.delimiter); if (valueToAppend.length === 0) { return vars; @@ -61,3 +121,114 @@ export class EnvironmentVariablesService implements IEnvironmentVariablesService return vars; } } + +export function parseEnvFile(lines: string | Buffer, baseVars?: EnvironmentVariables): EnvironmentVariables { + const globalVars = baseVars ? baseVars : {}; + const vars: EnvironmentVariables = {}; + lines + .toString() + .split('\n') + .forEach((line, _idx) => { + const [name, value] = parseEnvLine(line); + if (name === '') { + return; + } + vars[name] = substituteEnvVars(value, vars, globalVars); + }); + return vars; +} + +function parseEnvLine(line: string): [string, string] { + // Most of the following is an adaptation of the dotenv code: + // https://github.com/motdotla/dotenv/blob/master/lib/main.js#L32 + // We don't use dotenv here because it loses ordering, which is + // significant for substitution. + const match = line.match(/^\s*(_*[a-zA-Z]\w*)\s*=\s*(.*?)?\s*$/); + if (!match) { + return ['', '']; + } + + const name = match[1]; + let value = match[2]; + if (value && value !== '') { + if (value[0] === "'" && value[value.length - 1] === "'") { + value = value.substring(1, value.length - 1); + value = value.replace(/\\n/gm, '\n'); + } else if (value[0] === '"' && value[value.length - 1] === '"') { + value = value.substring(1, value.length - 1); + value = value.replace(/\\n/gm, '\n'); + } + } else { + value = ''; + } + + return [name, value]; +} + +const SUBST_REGEX = /\${([a-zA-Z]\w*)?([^}\w].*)?}/g; + +function substituteEnvVars( + value: string, + localVars: EnvironmentVariables, + globalVars: EnvironmentVariables, + missing = '', +): string { + // Substitution here is inspired a little by dotenv-expand: + // https://github.com/motdotla/dotenv-expand/blob/master/lib/main.js + + let invalid = false; + let replacement = value; + replacement = replacement.replace(SUBST_REGEX, (match, substName, bogus, offset, orig) => { + if (offset > 0 && orig[offset - 1] === '\\') { + return match; + } + if ((bogus && bogus !== '') || !substName || substName === '') { + invalid = true; + return match; + } + return localVars[substName] || globalVars[substName] || missing; + }); + if (!invalid && replacement !== value) { + value = replacement; + sendTelemetryEvent(EventName.ENVFILE_VARIABLE_SUBSTITUTION); + } + + return value.replace(/\\\$/g, '$'); +} + +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 046927903e32..14573d2204aa 100644 --- a/src/client/common/variables/environmentVariablesProvider.ts +++ b/src/client/common/variables/environmentVariablesProvider.ts @@ -2,79 +2,188 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Disposable, Event, EventEmitter, FileSystemWatcher, Uri, workspace } from 'vscode'; +import * as path from 'path'; +import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, FileSystemWatcher, Uri } from 'vscode'; +import { traceError, traceVerbose } from '../../logging'; +import { sendFileCreationTelemetry } from '../../telemetry/envFileTelemetry'; +import { IWorkspaceService } from '../application/types'; +import { PythonSettings } from '../configSettings'; import { IPlatformService } from '../platform/types'; -import { IConfigurationService, ICurrentProcess, IDisposableRegistry } from '../types'; +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 { - private cache = new Map<string, EnvironmentVariables>(); + public trackedWorkspaceFolders = new Set<string>(); + private fileWatchers = new Map<string, FileSystemWatcher>(); + private disposables: Disposable[] = []; + private changeEventEmitter: EventEmitter<Uri | undefined>; - constructor(@inject(IEnvironmentVariablesService) private envVarsService: IEnvironmentVariablesService, + + private readonly envVarCaches = new Map<string, InMemoryCache<EnvironmentVariables>>(); + + constructor( + @inject(IEnvironmentVariablesService) private envVarsService: IEnvironmentVariablesService, @inject(IDisposableRegistry) disposableRegistry: Disposable[], @inject(IPlatformService) private platformService: IPlatformService, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(ICurrentProcess) private process: ICurrentProcess) { + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(ICurrentProcess) private process: ICurrentProcess, + private cacheDuration: number = CACHE_DURATION, + ) { disposableRegistry.push(this); this.changeEventEmitter = new EventEmitter(); + const disposable = this.workspaceService.onDidChangeConfiguration(this.configurationChanged, this); + this.disposables.push(disposable); } public get onDidEnvironmentVariablesChange(): Event<Uri | undefined> { return this.changeEventEmitter.event; } - public dispose() { + public dispose(): void { this.changeEventEmitter.dispose(); - this.fileWatchers.forEach(watcher => { - watcher.dispose(); + this.fileWatchers.forEach((watcher) => { + if (watcher) { + watcher.dispose(); + } }); } + public async getEnvironmentVariables(resource?: Uri): Promise<EnvironmentVariables> { - const settings = this.configurationService.getSettings(resource); - if (!this.cache.has(settings.envFile)) { - const workspaceFolderUri = this.getWorkspaceFolderUri(resource); - this.createFileWatcher(settings.envFile, workspaceFolderUri); - let mergedVars = await this.envVarsService.parseFile(settings.envFile); - if (!mergedVars) { - mergedVars = {}; - } - this.envVarsService.mergeVariables(this.process.env, mergedVars!); - const pathVariable = this.platformService.pathVariableName; - const pathValue = this.process.env[pathVariable]; - if (pathValue) { - this.envVarsService.appendPath(mergedVars!, pathValue); - } - if (this.process.env.PYTHONPATH) { - this.envVarsService.appendPythonPath(mergedVars!, this.process.env.PYTHONPATH); + 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 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 }; } - this.cache.set(settings.envFile, mergedVars); } - return this.cache.get(settings.envFile)!; + return undefined; } - private getWorkspaceFolderUri(resource?: Uri): Uri | undefined { - if (!resource) { - return; + + private setCachedEnvironmentVariables(resource: Uri | undefined, vars: EnvironmentVariables): void { + const cacheKey = this.getWorkspaceFolderUri(resource)?.fsPath ?? ''; + const cache = new InMemoryCache<EnvironmentVariables>(this.cacheDuration); + this.envVarCaches.set(cacheKey, cache); + cache.data = { ...vars }; + } + + public async _getEnvironmentVariables(resource?: Uri): Promise<EnvironmentVariables> { + 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 = {}; } - const workspaceFolder = workspace.getWorkspaceFolder(resource!); - return workspaceFolder ? workspaceFolder.uri : undefined; + this.envVarsService.mergeVariables(this.process.env, mergedVars!); + const pathVariable = this.platformService.pathVariableName; + const pathValue = this.process.env[pathVariable]; + if (pathValue) { + this.envVarsService.appendPath(mergedVars!, pathValue); + } + if (this.process.env.PYTHONPATH) { + this.envVarsService.appendPythonPath(mergedVars!, this.process.env.PYTHONPATH); + } + return mergedVars; + } + + public async getCustomEnvironmentVariables(resource?: Uri): Promise<EnvironmentVariables | undefined> { + 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<string>('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(envFile, workspaceFolderUri); + return envFile; + } + + 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)) { + this.onEnvironmentFileChanged(uri); + } + }); } - private createFileWatcher(envFile: string, workspaceFolderUri?: Uri) { + + public createFileWatcher(envFile: string, workspaceFolderUri?: Uri): void { if (this.fileWatchers.has(envFile)) { return; } - const envFileWatcher = workspace.createFileSystemWatcher(envFile); + const envFileWatcher = this.workspaceService.createFileSystemWatcher(envFile); this.fileWatchers.set(envFile, envFileWatcher); if (envFileWatcher) { - this.disposables.push(envFileWatcher.onDidChange(() => this.onEnvironmentFileChanged(envFile, workspaceFolderUri))); - this.disposables.push(envFileWatcher.onDidCreate(() => this.onEnvironmentFileChanged(envFile, workspaceFolderUri))); - this.disposables.push(envFileWatcher.onDidDelete(() => this.onEnvironmentFileChanged(envFile, workspaceFolderUri))); + this.disposables.push(envFileWatcher.onDidChange(() => this.onEnvironmentFileChanged(workspaceFolderUri))); + this.disposables.push(envFileWatcher.onDidCreate(() => this.onEnvironmentFileCreated(workspaceFolderUri))); + this.disposables.push(envFileWatcher.onDidDelete(() => this.onEnvironmentFileChanged(workspaceFolderUri))); + } + } + + private getWorkspaceFolderUri(resource?: Uri): Uri | undefined { + if (!resource) { + return undefined; } + const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource!); + return workspaceFolder ? workspaceFolder.uri : undefined; } - private onEnvironmentFileChanged(envFile, workspaceFolderUri?: Uri) { - this.cache.delete(envFile); + + private onEnvironmentFileCreated(workspaceFolderUri?: Uri) { + this.onEnvironmentFileChanged(workspaceFolderUri); + sendFileCreationTelemetry(); + } + + private onEnvironmentFileChanged(workspaceFolderUri?: Uri) { + // 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 957f462e52eb..db4f620ab6a7 100644 --- a/src/client/common/variables/serviceRegistry.ts +++ b/src/client/common/variables/serviceRegistry.ts @@ -7,6 +7,12 @@ import { EnvironmentVariablesProvider } from './environmentVariablesProvider'; import { IEnvironmentVariablesProvider, IEnvironmentVariablesService } from './types'; export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton<IEnvironmentVariablesService>(IEnvironmentVariablesService, EnvironmentVariablesService); - serviceManager.addSingleton<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider, EnvironmentVariablesProvider); + serviceManager.addSingleton<IEnvironmentVariablesService>( + IEnvironmentVariablesService, + EnvironmentVariablesService, + ); + serviceManager.addSingleton<IEnvironmentVariablesProvider>( + IEnvironmentVariablesProvider, + EnvironmentVariablesProvider, + ); } diff --git a/src/client/common/variables/sysTypes.ts b/src/client/common/variables/sysTypes.ts deleted file mode 100644 index 108862392e04..000000000000 --- a/src/client/common/variables/sysTypes.ts +++ /dev/null @@ -1,38 +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 6f7ab78e37b5..05e5d9d6f584 100644 --- a/src/client/common/variables/systemVariables.ts +++ b/src/client/common/variables/systemVariables.ts @@ -2,22 +2,22 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - 'use strict'; - import * as Path from 'path'; +import { Range, Uri } from 'vscode'; + +import { IDocumentManager, IWorkspaceService } from '../application/types'; +import { 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 */ - -export abstract class AbstractSystemVariables implements ISystemVariables { +abstract class AbstractSystemVariables implements ISystemVariables { public resolve(value: string): string; public resolve(value: string[]): string[]; public resolve(value: IStringDictionary<string>): IStringDictionary<string>; public resolve(value: IStringDictionary<string[]>): IStringDictionary<string[]>; public resolve(value: IStringDictionary<IStringDictionary<string>>): IStringDictionary<IStringDictionary<string>>; - // tslint:disable-next-line:no-any + public resolve(value: any): any { if (Types.isString(value)) { return this.__resolveString(value); @@ -31,7 +31,7 @@ export abstract class AbstractSystemVariables implements ISystemVariables { } public resolveAny<T>(value: T): T; - // tslint:disable-next-line:no-any + public resolveAny(value: any): any { if (Types.isString(value)) { return this.__resolveString(value); @@ -47,7 +47,6 @@ export 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 = (<any>this)[name]; if (Types.isString(newValue)) { return newValue; @@ -57,50 +56,88 @@ export abstract class AbstractSystemVariables implements ISystemVariables { }); } - private __resolveLiteral(values: IStringDictionary<string | IStringDictionary<string> | string[]>): IStringDictionary<string | IStringDictionary<string> | string[]> { + private __resolveLiteral( + values: IStringDictionary<string | IStringDictionary<string> | string[]>, + ): IStringDictionary<string | IStringDictionary<string> | string[]> { const result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null); - Object.keys(values).forEach(key => { + Object.keys(values).forEach((key) => { const value = values[key]; - // tslint:disable-next-line:no-any + result[key] = <any>this.resolve(<any>value); }); return result; } private __resolveAnyLiteral<T>(values: T): T; - // tslint:disable-next-line:no-any + private __resolveAnyLiteral(values: any): any { const result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null); - Object.keys(values).forEach(key => { + Object.keys(values).forEach((key) => { const value = values[key]; - // tslint:disable-next-line:no-any + result[key] = <any>this.resolveAny(<any>value); }); return result; } private __resolveArray(value: string[]): string[] { - return value.map(s => this.__resolveString(s)); + return value.map((s) => this.__resolveString(s)); } private __resolveAnyArray<T>(value: T[]): T[]; - // tslint:disable-next-line:no-any + private __resolveAnyArray(value: any[]): any[] { - return value.map(s => this.resolveAny(s)); + return value.map((s) => this.resolveAny(s)); } } export class SystemVariables extends AbstractSystemVariables { private _workspaceFolder: string; private _workspaceFolderName: string; - - constructor(workspaceFolder?: string) { + private _filePath: string | undefined; + private _lineNumber: number | undefined; + private _selectedText: string | undefined; + private _execPath: string; + + constructor( + file: Uri | undefined, + rootFolder: string | undefined, + workspace?: IWorkspaceService, + documentManager?: IDocumentManager, + ) { super(); - this._workspaceFolder = typeof workspaceFolder === 'string' ? workspaceFolder : __dirname; + const workspaceFolder = workspace && file ? workspace.getWorkspaceFolder(file) : undefined; + this._workspaceFolder = workspaceFolder ? workspaceFolder.uri.fsPath : rootFolder || __dirname; this._workspaceFolderName = Path.basename(this._workspaceFolder); - Object.keys(process.env).forEach(key => { - (this as any as { [key: string]: string | undefined })[`env:${key}`] = (this as any as { [key: string]: string | undefined })[`env.${key}`] = process.env[key]; + this._filePath = file ? file.fsPath : undefined; + if (documentManager && documentManager.activeTextEditor) { + this._lineNumber = documentManager.activeTextEditor.selection.anchor.line + 1; + this._selectedText = documentManager.activeTextEditor.document.getText( + new Range( + documentManager.activeTextEditor.selection.start, + documentManager.activeTextEditor.selection.end, + ), + ); + } + this._execPath = process.execPath; + Object.keys(process.env).forEach((key) => { + ((this as any) as Record<string, string | undefined>)[`env:${key}`] = ((this as any) as Record< + string, + 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<string, string | undefined>)[`workspaceFolder:${basename}`] = + folder.uri.fsPath; + ((this as any) as Record<string, string | undefined>)[`workspaceFolder:${folder.name}`] = + folder.uri.fsPath; + }); + } catch { + // This try...catch block is here to support pre-existing tests, ignore error. + } } public get cwd(): string { @@ -122,4 +159,44 @@ export class SystemVariables extends AbstractSystemVariables { public get workspaceFolderBasename(): string { return this._workspaceFolderName; } + + public get file(): string | undefined { + return this._filePath; + } + + public get relativeFile(): string | undefined { + return this.file ? Path.relative(this._workspaceFolder, this.file) : undefined; + } + + public get relativeFileDirname(): string | undefined { + return this.relativeFile ? Path.dirname(this.relativeFile) : undefined; + } + + public get fileBasename(): string | undefined { + return this.file ? Path.basename(this.file) : undefined; + } + + public get fileBasenameNoExtension(): string | undefined { + return this.file ? Path.parse(this.file).name : undefined; + } + + public get fileDirname(): string | undefined { + return this.file ? Path.dirname(this.file) : undefined; + } + + public get fileExtname(): string | undefined { + return this.file ? Path.extname(this.file) : undefined; + } + + public get lineNumber(): number | undefined { + return this._lineNumber; + } + + public get selectedText(): string | undefined { + return this._selectedText; + } + + public get execPath(): string { + return this._execPath; + } } diff --git a/src/client/common/variables/types.ts b/src/client/common/variables/types.ts index bfdd40e0890c..252a0d48038f 100644 --- a/src/client/common/variables/types.ts +++ b/src/client/common/variables/types.ts @@ -3,15 +3,18 @@ import { Event, Uri } from 'vscode'; -export type EnvironmentVariables = Object & { - [key: string]: string | undefined; -}; +export type EnvironmentVariables = Object & Record<string, string | undefined>; export const IEnvironmentVariablesService = Symbol('IEnvironmentVariablesService'); export interface IEnvironmentVariablesService { - parseFile(filePath?: string): Promise<EnvironmentVariables | undefined>; - mergeVariables(source: EnvironmentVariables, target: EnvironmentVariables): void; + parseFile(filePath?: string, baseVars?: EnvironmentVariables): Promise<EnvironmentVariables | undefined>; + 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; } @@ -31,7 +34,7 @@ export interface ISystemVariables { resolve(value: IStringDictionary<string[]>): IStringDictionary<string[]>; resolve(value: IStringDictionary<IStringDictionary<string>>): IStringDictionary<IStringDictionary<string>>; resolveAny<T>(value: T): T; - // tslint:disable-next-line:no-any + [key: string]: any; } @@ -40,4 +43,5 @@ export const IEnvironmentVariablesProvider = Symbol('IEnvironmentVariablesProvid export interface IEnvironmentVariablesProvider { onDidEnvironmentVariablesChange: Event<Uri | undefined>; getEnvironmentVariables(resource?: Uri): Promise<EnvironmentVariables>; + 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<T>(command: string, ...rest: any[]): Thenable<T> { + return commands.executeCommand<T>(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<T = unknown>(extensionId: string): vscode.Extension<T> | 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<any>[] { + 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<TextEditor> { + return window.showTextDocument(uri); +} + +export function showNotebookDocument( + document: NotebookDocument, + options?: NotebookDocumentShowOptions, +): Thenable<NotebookEditor> { + return window.showNotebookDocument(document, options); +} + +export function showQuickPick<T extends QuickPickItem>( + items: readonly T[] | Thenable<readonly T[]>, + options?: QuickPickOptions, + token?: CancellationToken, +): Thenable<T | undefined> { + return window.showQuickPick(items, options, token); +} + +export function createQuickPick<T extends QuickPickItem>(): QuickPick<T> { + return window.createQuickPick<T>(); +} + +export function showErrorMessage<T extends string>(message: string, ...items: T[]): Thenable<T | undefined>; +export function showErrorMessage<T extends string>( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable<T | undefined>; +export function showErrorMessage<T extends MessageItem>(message: string, ...items: T[]): Thenable<T | undefined>; +export function showErrorMessage<T extends MessageItem>( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable<T | undefined>; + +export function showErrorMessage<T>(message: string, ...items: any[]): Thenable<T | undefined> { + return window.showErrorMessage(message, ...items); +} + +export function showWarningMessage<T extends string>(message: string, ...items: T[]): Thenable<T | undefined>; +export function showWarningMessage<T extends string>( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable<T | undefined>; +export function showWarningMessage<T extends MessageItem>(message: string, ...items: T[]): Thenable<T | undefined>; +export function showWarningMessage<T extends MessageItem>( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable<T | undefined>; + +export function showWarningMessage<T>(message: string, ...items: any[]): Thenable<T | undefined> { + return window.showWarningMessage(message, ...items); +} + +export function showInformationMessage<T extends string>(message: string, ...items: T[]): Thenable<T | undefined>; +export function showInformationMessage<T extends string>( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable<T | undefined>; +export function showInformationMessage<T extends MessageItem>(message: string, ...items: T[]): Thenable<T | undefined>; +export function showInformationMessage<T extends MessageItem>( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable<T | undefined>; + +export function showInformationMessage<T>(message: string, ...items: any[]): Thenable<T | undefined> { + return window.showInformationMessage(message, ...items); +} + +export function withProgress<R>( + options: ProgressOptions, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable<R>, +): Thenable<R> { + 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<T extends QuickPickItem>( + items: readonly T[], + options?: QuickPickOptions, + token?: CancellationToken, + itemButtonHandler?: (e: QuickPickItemButtonEvent<T>) => void, +): Promise<T | T[] | undefined> { + const quickPick: QuickPick<T> = window.createQuickPick<T>(); + 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<T | T[] | undefined>(); + + 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<MultiStepAction>, + public next: MultiStepNode | undefined, + ) {} + + public static async run(step: MultiStepNode, context?: MultiStepAction): Promise<MultiStepAction> { + 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<T>(deferred?: Deferred<T>): 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<T>(deferred?: Deferred<T>, 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<boolean> { + return vscode.workspace.applyEdit(edit); +} + +export function findFiles( + include: vscode.GlobPattern, + exclude?: vscode.GlobPattern | null, + maxResults?: number, + token?: vscode.CancellationToken, +): Thenable<vscode.Uri[]> { + 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<void> { + return vscode.workspace.fs.createDirectory(uri); +} + +export function openNotebookDocument(uri: vscode.Uri): Thenable<vscode.NotebookDocument>; +export function openNotebookDocument( + notebookType: string, + content?: vscode.NotebookData, +): Thenable<vscode.NotebookDocument>; +export function openNotebookDocument(notebook: any, content?: vscode.NotebookData): Thenable<vscode.NotebookDocument> { + return vscode.workspace.openNotebookDocument(notebook, content); +} + +export function copy(source: vscode.Uri, dest: vscode.Uri, options?: { overwrite?: boolean }): Thenable<void> { + 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<void>; +}; diff --git a/src/client/constants.ts b/src/client/constants.ts index d0b1f89025b6..48c5f55e5ce4 100644 --- a/src/client/constants.ts +++ b/src/client/constants.ts @@ -8,4 +8,7 @@ import * as path from 'path'; // This file is also used by the debug adapter. // When bundling, the bundle file for the debug adapter ends up elsewhere. const folderName = path.basename(__dirname); -export const EXTENSION_ROOT_DIR = folderName === 'client' ? path.join(__dirname, '..', '..') : path.join(__dirname, '..', '..', '..', '..'); +export const EXTENSION_ROOT_DIR = + folderName === 'client' ? path.join(__dirname, '..', '..') : path.join(__dirname, '..', '..', '..', '..'); + +export const HiddenFilePrefix = '_HiddenFile_'; diff --git a/src/client/datascience/cellFactory.ts b/src/client/datascience/cellFactory.ts deleted file mode 100644 index 2b86c9abb57c..000000000000 --- a/src/client/datascience/cellFactory.ts +++ /dev/null @@ -1,133 +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 } from 'vscode'; - -import { RegExpValues } from './constants'; -import { CellState, ICell } from './types'; - -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`; - }); -} - -function generateCodeCell(code: string[], file: string, line: number) : ICell { - // Code cells start out with just source and no outputs. - return { - data: { - source: appendLineFeed(code), - cell_type: 'code', - outputs: [], - metadata: {}, - execution_count: 0 - }, - id: uuid(), - file: file, - line: line, - state: CellState.init - }; - -} - -function generateMarkdownCell(code: string[], file: string, line: number) : ICell { - // Generate markdown by stripping out the comment and markdown header - const markdown = appendLineFeed(code.slice(1).filter(s => s.includes('#')), s => s.trim().slice(1).trim()); - - return { - id: uuid(), - file: file, - line: line, - state: CellState.finished, - data: { - cell_type: 'markdown', - source: markdown, - metadata: {} - } - }; - -} - -export function generateCells(code: string, file: string, line: number, splitMarkdown?: boolean) : 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]; - if (RegExpValues.PythonMarkdownCellMarker.test(firstLine)) { - // We have at least one markdown. We might have to split it if there any lines that don't begin - // with # - const firstNonMarkdown = splitMarkdown ? split.findIndex((l: string) => l.trim().length > 0 && !l.trim().startsWith('#')) : -1; - if (firstNonMarkdown >= 0) { - return [ - generateMarkdownCell(split.slice(0, firstNonMarkdown), file, line), - generateCodeCell(split.slice(firstNonMarkdown), file, line + firstNonMarkdown) - ]; - } else { - // Just a single markdown cell - return [generateMarkdownCell(split, file, line)]; - } - } else { - // Just code - return [generateCodeCell(split, file, line)]; - } -} - -export function hasCells(document: TextDocument) : boolean { - const cellIdentifier: RegExp = RegExpValues.PythonCellMarker; - for (let index = 0; index < document.lineCount; index += 1) { - const line = document.lineAt(index); - // clear regex cache - if (cellIdentifier.test(line.text)) { - return true; - } - } - - return false; -} - -export function generateCellRanges(document: TextDocument) : {range: Range; title: string}[] { - // Implmentation of getCells here based on Don's Jupyter extension work - const cellIdentifier: RegExp = RegExpValues.PythonCellMarker; - const cells : {range: Range; title: string}[] = []; - for (let index = 0; index < document.lineCount; index += 1) { - const line = document.lineAt(index); - // clear regex cache - cellIdentifier.lastIndex = -1; - if (cellIdentifier.test(line.text)) { - const results = cellIdentifier.exec(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); - } - - if (results !== null) { - cells.push({ - range: line.range, - title: results.length > 1 ? results[2].trim() : '' - }); - } - } - } - - 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) : ICell[] { - // Get our ranges. They'll determine our cells - const ranges = generateCellRanges(document); - - // For each one, get its text and turn it into a cell - return Array.prototype.concat(...ranges.map(r => { - const code = document.getText(r.range); - return generateCells(code, document.fileName, r.range.start.line); - })); -} diff --git a/src/client/datascience/codeCssGenerator.ts b/src/client/datascience/codeCssGenerator.ts deleted file mode 100644 index 704349a7cff2..000000000000 --- a/src/client/datascience/codeCssGenerator.ts +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { JSONArray, JSONObject, JSONValue } from '@phosphor/coreutils'; -import { FindOptions } from 'file-matcher'; -import * as fs from 'fs-extra'; -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { IWorkspaceService } from '../common/application/types'; -import { ICurrentProcess, ILogger } from '../common/types'; -import { EXTENSION_ROOT_DIR } from '../constants'; -import { ICodeCssGenerator } from './types'; - -// 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(ICurrentProcess) private currentProcess: ICurrentProcess, - @inject(ILogger) private logger: ILogger) { - } - - public generateThemeCss = async (): Promise<string> => { - try { - // First compute our current theme. - const workbench = this.workspaceService.getConfiguration('workbench'); - const theme = workbench.get<string>('colorTheme'); - const editor = this.workspaceService.getConfiguration('editor', undefined); - const font = editor.get<string>('fontFamily'); - const fontSize = editor.get<number>('fontSize'); - - // Then we have to find where the theme resources are loaded from - if (theme) { - const tokenColors = await this.findTokenColors(theme); - - // The tokens object then contains the necessary data to generate our css - if (tokenColors && font && fontSize) { - return this.generateCss(tokenColors, font, fontSize); - } - } - } catch (err) { - // On error don't fail, just log - this.logger.logError(err); - } - - return ''; - } - - private getScopeColor = (tokenColors: JSONArray, scope: string): string => { - // Search through the scopes on the json object - const match = tokenColors.findIndex(entry => { - if (entry) { - const scopes = entry['scope'] as JSONValue; - if (scopes && Array.isArray(scopes)) { - if (scopes.find(v => v !== null && v.toString() === scope)) { - return true; - } - } else if (scopes && scopes.toString() === scope) { - return true; - } - } - - return false; - }); - - const found = match >= 0 ? tokenColors[match] : null; - if (found !== null) { - const settings = found['settings']; - if (settings && settings !== null) { - return settings['foreground']; - } - } - - // Default to editor foreground - return 'var(--vscode-editor-foreground)'; - } - - // tslint:disable-next-line:max-func-body-length - private generateCss = (tokenColors: JSONArray, fontFamily: string, fontSize: number): string => { - - // There's a set of values that need to be found - const comment = this.getScopeColor(tokenColors, 'comment'); - const numeric = this.getScopeColor(tokenColors, 'constant.numeric'); - const stringColor = this.getScopeColor(tokenColors, 'string'); - const keyword = this.getScopeColor(tokenColors, 'keyword'); - const operator = this.getScopeColor(tokenColors, 'keyword.operator'); - const variable = this.getScopeColor(tokenColors, 'variable'); - const def = 'var(--vscode-editor-foreground)'; - - // Use these values to fill in our format string - return ` - :root { - --comment-color: ${comment} - } - code[class*="language-"], - pre[class*="language-"] { - color: ${def}; - background: none; - text-shadow: none; - font-family: ${fontFamily}; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - font-size: ${fontSize}px; - - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; - } - - pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, - code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { - text-shadow: none; - background: var(--vscode-editor-selectionBackground); - } - - pre[class*="language-"]::selection, pre[class*="language-"] ::selection, - code[class*="language-"]::selection, code[class*="language-"] ::selection { - text-shadow: none; - background: var(--vscode-editor-selectionBackground); - } - - @media print { - code[class*="language-"], - pre[class*="language-"] { - text-shadow: none; - } - } - - /* Code blocks */ - pre[class*="language-"] { - padding: 1em; - margin: .5em 0; - overflow: auto; - } - - :not(pre) > code[class*="language-"], - pre[class*="language-"] { - background: transparent; - } - - /* Inline code */ - :not(pre) > code[class*="language-"] { - padding: .1em; - border-radius: .3em; - white-space: normal; - } - - .token.comment, - .token.prolog, - .token.doctype, - .token.cdata { - color: ${comment}; - } - - .token.punctuation { - color: ${def}; - } - - .namespace { - opacity: .7; - } - - .token.property, - .token.tag, - .token.boolean, - .token.number, - .token.constant, - .token.symbol, - .token.deleted { - color: ${numeric}; - } - - .token.selector, - .token.attr-name, - .token.string, - .token.char, - .token.builtin, - .token.inserted { - color: ${stringColor}; - } - - .token.operator, - .token.entity, - .token.url, - .language-css .token.string, - .style .token.string { - color: ${operator}; - background: transparent; - } - - .token.atrule, - .token.attr-value, - .token.keyword { - color: ${keyword}; - } - - .token.function, - .token.class-name { - color: ${keyword}; - } - - .token.regex, - .token.important, - .token.variable { - color: ${variable}; - } - - .token.important, - .token.bold { - font-weight: bold; - } - .token.italic { - font-style: italic; - } - - .token.entity { - cursor: help; - } -`; - - } - - private mergeColors = (colors1: JSONArray, colors2: JSONArray): JSONArray => { - return [...colors1, ...colors2]; - } - - private readTokenColors = async (themeFile: string): Promise<JSONArray> => { - const tokenContent = await fs.readFile(themeFile, 'utf8'); - const theme = JSON.parse(tokenContent) as JSONObject; - const 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 && include !== null) { - 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; - } - - return []; - } - - private findTokenColors = async (theme: string): Promise<JSONArray> => { - 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 fs.pathExists(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 json files for the theme name - const escapedThemeName = theme.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - const searchOptions: FindOptions = { - path: extensionsPath, - recursiveSearch: true, - fileFilter: { - fileNamePattern: '**/*.json', - content: new RegExp(`id[',"]:\\s*[',"]${escapedThemeName}[',"]`) - } - }; - // tslint:disable-next-line:no-require-imports - const fm = require('file-matcher') as typeof import('file-matcher'); - const matcher = new fm.FileMatcher(); - - try { - const results = await matcher.find(searchOptions); - - // Use the first result if we have one - if (results && results.length > 0) { - // This should be the path to the file. Load it as a json object - const contents = await fs.readFile(results[0], 'utf8'); - const json = JSON.parse(contents) as JSONObject; - - // There should be a contributes section - const contributes = json['contributes'] as JSONObject; - - // 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 => { - return e !== null && e['id'] === theme; - }); - - const found = index >= 0 ? themes[index] : 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(results[0]), found['path']); - return await this.readTokenColors(themeFile); - } - } - } catch (err) { - // Swallow any exceptions with searching or parsing - this.logger.logError(err); - } - - // We should return a default. The vscode-light theme - const defaultThemeFile = path.join(EXTENSION_ROOT_DIR, 'resources', 'defaultTheme.json'); - return this.readTokenColors(defaultThemeFile); - } -} diff --git a/src/client/datascience/common.ts b/src/client/datascience/common.ts deleted file mode 100644 index 6d33babb85b8..000000000000 --- a/src/client/datascience/common.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import { nbformat } from '@jupyterlab/coreutils/lib/nbformat'; - -export function concatMultilineString(str : nbformat.MultilineString) : string { - 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); - } - } - return result.trim(); - } - return str.toString().trim(); -} - -export function formatStreamText(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; -} diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts deleted file mode 100644 index 196872ec57ac..000000000000 --- a/src/client/datascience/constants.ts +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -export namespace Commands { - export const RunAllCells = 'python.datascience.runallcells'; - export const RunCell = 'python.datascience.runcell'; - export const RunCurrentCell = 'python.datascience.runcurrentcell'; - export const RunCurrentCellAdvance = 'python.datascience.runcurrentcelladvance'; - export const ShowHistoryPane = 'python.datascience.showhistorypane'; - export const ImportNotebook = 'python.datascience.importnotebook'; - export const SelectJupyterURI = 'python.datascience.selectjupyteruri'; - 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 ExpandAllCells = 'python.datascience.expandallcells'; - export const CollapseAllCells = 'python.datascience.collapseallcells'; - export const ExportOutputAsNotebook = 'python.datascience.exportoutputasnotebook'; -} - -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 namespace RegExpValues { - export const PythonCellMarker = new RegExp('^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])(.*)'); - export const PythonMarkdownCellMarker = /^#\s*%%\s*\[markdown\]/; -} - -export namespace HistoryMessages { - export const StartCell = 'start_cell'; - export const FinishCell = 'finish_cell'; - export const UpdateCell = 'update_cell'; - export const GotoCodeCell = 'gotocell_code'; - export const RestartKernel = 'restart_kernel'; - export const Export = 'export_to_ipynb'; - export const GetAllCells = 'get_all_cells'; - export const ReturnAllCells = 'return_all_cells'; - export const DeleteCell = 'delete_cell'; - export const DeleteAllCells = 'delete_all_cells'; - export const Undo = 'undo'; - export const Redo = 'redo'; - export const ExpandAll = 'expand_all'; - export const CollapseAll = 'collapse_all'; - export const StartProgress = 'start_progress'; - export const StopProgress = 'stop_progress'; - export const Interrupt = 'interrupt'; - export const SendInfo = 'send_info'; -} - -export namespace Telemetry { - export const ImportNotebook = 'DATASCIENCE.IMPORT_NOTEBOOK'; - export const RunCell = 'DATASCIENCE.RUN_CELL'; - export const RunCurrentCell = 'DATASCIENCE.RUN_CURRENT_CELL'; - export const RunCurrentCellAndAdvance = 'DATASCIENCE.RUN_CURRENT_CELL_AND_ADVANCE'; - export const RunAllCells = 'DATASCIENCE.RUN_ALL_CELLS'; - export const DeleteAllCells = 'DATASCIENCE.DELETE_ALL_CELLS'; - export const DeleteCell = 'DATASCIENCE.DELETE_CELL'; - export const GotoSourceCode = 'DATASCIENCE.GOTO_SOURCE'; - export const RestartKernel = 'DATASCIENCE.RESTART_KERNEL'; - export const ExportNotebook = 'DATASCIENCE.EXPORT_NOTEBOOK'; - export const Undo = 'DATASCIENCE.UNDO'; - export const Redo = 'DATASCIENCE.REDO'; - export const ShowHistoryPane = 'DATASCIENCE.SHOW_HISTORY_PANE'; - export const ExpandAll = 'DATASCIENCE.EXPAND_ALL'; - export const CollapseAll = 'DATASCIENCE.COLLAPSE_ALL'; - export const SelectJupyterURI = 'DATASCIENCE.SELECT_JUPYTER_URI'; - export const SetJupyterURIToLocal = 'DATASCIENCE.SET_JUPYTER_URI_LOCAL'; - export const SetJupyterURIToUserSpecified = 'DATASCIENCE.SET_JUPYTER_URI_USER_SPECIFIED'; - export const Interrupt = 'DATASCIENCE.INTERRUPT'; - export const ExportPythonFile = 'DATASCIENCE.EXPORT_PYTHON_FILE'; - export const ExportPythonFileAndOutput = 'DATASCIENCE.EXPORT_PYTHON_FILE_AND_OUTPUT'; - export const StartJupyter = 'DATASCIENCE.JUPYTERSTARTUPCOST'; -} - -export namespace HelpLinks { - export const PythonInteractiveHelpLink = 'https://aka.ms/pyaiinstall'; -} - -export namespace Settings { - export const JupyterServerLocalLaunch = 'local'; -} - -export namespace CodeSnippits { - export const ChangeDirectory = ['{0}', 'import os', 'try:', '\tos.chdir(os.path.join(os.getcwd(), \'{1}\'))', '\tprint(os.getcwd())', 'except:', '\tpass', '']; -} diff --git a/src/client/datascience/dataScienceSurveyBanner.ts b/src/client/datascience/dataScienceSurveyBanner.ts deleted file mode 100644 index 2ccca9ae9b49..000000000000 --- a/src/client/datascience/dataScienceSurveyBanner.ts +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { IApplicationShell } from '../common/application/types'; -import '../common/extensions'; -import { - IBrowserService, IPersistentStateFactory, - IPythonExtensionBanner -} from '../common/types'; -import * as localize from '../common/utils/localize'; - -export enum DSSurveyStateKeys { - ShowBanner = 'ShowDSSurveyBanner', - ShowAttemptCounter = 'DSSurveyShowAttempt' -} - -enum DSSurveyLabelIndex { - Yes, - No -} - -@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 commandThreshold: number; - private readonly surveyLink: string; - - constructor( - @inject(IApplicationShell) private appShell: IApplicationShell, - @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, - @inject(IBrowserService) private browserService: IBrowserService, - commandThreshold: number = 500, - surveyLink: string = 'https://aka.ms/pyaisurvey') { - this.commandThreshold = commandThreshold; - this.surveyLink = surveyLink; - this.initialize(); - } - - public initialize(): void { - if (this.isInitialized) { - return; - } - this.isInitialized = true; - } - - public get optionLabels(): string[] { - return this.bannerLabels; - } - - public get shownCount(): Promise<number> { - return this.getPythonDSCommandCounter(); - } - - public get enabled(): boolean { - return this.persistentState.createGlobalPersistentState<boolean>(DSSurveyStateKeys.ShowBanner, true).value; - } - - public async showBanner(): Promise<void> { - if (!this.enabled || this.disabledInCurrentSession) { - return; - } - - const launchCounter: number = await this.incrementPythonDataScienceCommandCounter(); - const show = await this.shouldShowBanner(launchCounter); - 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(launchCounter?: number): Promise<boolean> { - if (!this.enabled || this.disabledInCurrentSession) { - return false; - } - - if (!launchCounter) { - launchCounter = await this.getPythonDSCommandCounter(); - } - - return launchCounter >= this.commandThreshold; - } - - public async disable(): Promise<void> { - await this.persistentState.createGlobalPersistentState<boolean>(DSSurveyStateKeys.ShowBanner, false).updateValue(false); - } - - public async launchSurvey(): Promise<void> { - this.browserService.launch(this.surveyLink); - } - - private async getPythonDSCommandCounter(): Promise<number> { - const state = this.persistentState.createGlobalPersistentState<number>(DSSurveyStateKeys.ShowAttemptCounter, 0); - return state.value; - } - - private async incrementPythonDataScienceCommandCounter(): Promise<number> { - const state = this.persistentState.createGlobalPersistentState<number>(DSSurveyStateKeys.ShowAttemptCounter, 0); - await state.updateValue(state.value + 1); - return state.value; - } -} diff --git a/src/client/datascience/datascience.ts b/src/client/datascience/datascience.ts deleted file mode 100644 index 06a6396c8d70..000000000000 --- a/src/client/datascience/datascience.ts +++ /dev/null @@ -1,209 +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 { URL } from 'url'; -import * as vscode from 'vscode'; - -import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; -import { PythonSettings } from '../common/configSettings'; -import { PYTHON, PYTHON_LANGUAGE } from '../common/constants'; -import { ContextKey } from '../common/contextKey'; -import { - BANNER_NAME_DS_SURVEY, - IConfigurationService, - IDisposableRegistry, - IExtensionContext, - IPythonExtensionBanner -} from '../common/types'; -import * as localize from '../common/utils/localize'; -import { IServiceContainer } from '../ioc/types'; -import { captureTelemetry } from '../telemetry'; -import { hasCells } from './cellFactory'; -import { Commands, EditorContexts, Settings, Telemetry } from './constants'; -import { ICodeWatcher, IDataScience, IDataScienceCodeLensProvider, IDataScienceCommandListener } from './types'; - -@injectable() -export class DataScience implements IDataScience { - public isDisposed: boolean = false; - private readonly commandListeners: IDataScienceCommandListener[]; - private readonly dataScienceSurveyBanner: IPythonExtensionBanner; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, - @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(IApplicationShell) private appShell: IApplicationShell) { - this.commandListeners = this.serviceContainer.getAll<IDataScienceCommandListener>(IDataScienceCommandListener); - this.dataScienceSurveyBanner = this.serviceContainer.get<IPythonExtensionBanner>(IPythonExtensionBanner, BANNER_NAME_DS_SURVEY); - } - - public async activate(): Promise<void> { - this.registerCommands(); - - this.extensionContext.subscriptions.push( - vscode.languages.registerCodeLensProvider( - PYTHON, this.dataScienceCodeLensProvider - ) - ); - - // Set our initial settings and sign up for changes - this.onSettingsChanged(); - (this.configuration.getSettings() as PythonSettings).addListener('change', this.onSettingsChanged); - 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(); - } - - public async dispose() { - if (!this.isDisposed) { - this.isDisposed = true; - (this.configuration.getSettings() as PythonSettings).removeListener('change', this.onSettingsChanged); - } - } - - public async runAllCells(codeWatcher: ICodeWatcher): Promise<void> { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - let activeCodeWatcher: ICodeWatcher | undefined = codeWatcher; - if (!activeCodeWatcher) { - activeCodeWatcher = this.getCurrentCodeWatcher(); - } - if (activeCodeWatcher) { - return activeCodeWatcher.runAllCells(); - } else { - return Promise.resolve(); - } - } - - public async runCell(codeWatcher: ICodeWatcher, range: vscode.Range): Promise<void> { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - if (codeWatcher) { - return codeWatcher.runCell(range); - } else { - return this.runCurrentCell(); - } - } - - public async runCurrentCell(): Promise<void> { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.runCurrentCell(); - } else { - return Promise.resolve(); - } - } - - public async runCurrentCellAndAdvance(): Promise<void> { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.runCurrentCellAndAdvance(); - } else { - return Promise.resolve(); - } - } - - @captureTelemetry(Telemetry.SelectJupyterURI) - public async selectJupyterURI(): Promise<void> { - const quickPickOptions = [localize.DataScience.jupyterSelectURILaunchLocal(), localize.DataScience.jupyterSelectURISpecifyURI()]; - const selection = await this.appShell.showQuickPick(quickPickOptions); - switch (selection) { - case localize.DataScience.jupyterSelectURILaunchLocal(): - return this.setJupyterURIToLocal(); - break; - case localize.DataScience.jupyterSelectURISpecifyURI(): - return this.selectJupyterLaunchURI(); - break; - default: - // If user cancels quick pick we will get undefined as the selection and fall through here - break; - } - } - - @captureTelemetry(Telemetry.SetJupyterURIToLocal) - private async setJupyterURIToLocal(): Promise<void> { - await this.configuration.updateSetting('dataScience.jupyterServerURI', Settings.JupyterServerLocalLaunch, undefined, vscode.ConfigurationTarget.Workspace); - } - - @captureTelemetry(Telemetry.SetJupyterURIToUserSpecified) - private async selectJupyterLaunchURI(): Promise<void> { - // First get the proposed URI from the user - const userURI = await this.appShell.showInputBox({prompt: localize.DataScience.jupyterSelectURIPrompt(), - placeHolder: 'https://hostname:8080/?token=849d61a414abafab97bc4aab1f3547755ddc232c2b8cb7fe', validateInput: this.validateURI, ignoreFocusOut: true}); - - if (userURI) { - await this.configuration.updateSetting('dataScience.jupyterServerURI', userURI, undefined, vscode.ConfigurationTarget.Workspace); - } - } - - private validateURI = (testURI: string): string | undefined | null => { - try { - // tslint:disable-next-line:no-unused-expression - new URL(testURI); - } catch { - return localize.DataScience.jupyterSelectURIInvalidURI(); - } - - // Return null tells the dialog that our string is valid - return null; - } - - private onSettingsChanged = () => { - const settings = this.configuration.getSettings(); - const enabled = settings.datascience.enabled; - const editorContext = new ContextKey(EditorContexts.DataScienceEnabled, this.commandManager); - editorContext.set(enabled).catch(); - } - - // Get our matching code watcher for the active document - private getCurrentCodeWatcher(): ICodeWatcher | undefined { - const activeEditor = vscode.window.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 registerCommands(): void { - let disposable = this.commandManager.registerCommand(Commands.RunAllCells, this.runAllCells, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.RunCell, this.runCell, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.RunCurrentCell, this.runCurrentCell, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.RunCurrentCellAdvance, this.runCurrentCellAndAdvance, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.SelectJupyterURI, this.selectJupyterURI, this); - this.disposableRegistry.push(disposable); - this.commandListeners.forEach((listener: IDataScienceCommandListener) => { - listener.register(this.commandManager); - }); - } - - 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)).catch(); - } else { - editorContext.set(false).catch(); - } - } -} diff --git a/src/client/datascience/editor-integration/codelensprovider.ts b/src/client/datascience/editor-integration/codelensprovider.ts deleted file mode 100644 index 960c3b15b046..000000000000 --- a/src/client/datascience/editor-integration/codelensprovider.ts +++ /dev/null @@ -1,65 +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 { IConfigurationService } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { ICodeWatcher, IDataScienceCodeLensProvider } from '../types'; - -@injectable() -export class DataScienceCodeLensProvider implements IDataScienceCodeLensProvider { - private activeCodeWatchers: ICodeWatcher[] = []; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IConfigurationService) private configuration: IConfigurationService) - { - } - - // CodeLensProvider interface - // Some implementation based on DonJayamanne's jupyter extension work - public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): - vscode.CodeLens[] { - // Don't provide any code lenses if we have not enabled data science - const settings = this.configuration.getSettings(); - if (!settings.datascience.enabled) { - // 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 []; - } - - // See if we already have a watcher for this file and version - const codeWatcher: ICodeWatcher | undefined = this.matchWatcher(document.fileName, document.version); - if (codeWatcher) { - return codeWatcher.getCodeLenses(); - } - - // Create a new watcher for this file - const newCodeWatcher = this.serviceContainer.get<ICodeWatcher>(ICodeWatcher); - newCodeWatcher.addFile(document); - this.activeCodeWatchers.push(newCodeWatcher); - return newCodeWatcher.getCodeLenses(); - } - - // IDataScienceCodeLensProvider interface - public getCodeWatcher(document: vscode.TextDocument): ICodeWatcher | undefined { - return this.matchWatcher(document.fileName, document.version); - } - - private matchWatcher(fileName: string, version: number) : ICodeWatcher | undefined { - const index = this.activeCodeWatchers.findIndex(item => item.getFileName() === fileName); - if (index >= 0) { - const item = this.activeCodeWatchers[index]; - if (item.getVersion() === version) { - return item; - } - // If we have an old version remove it from the active list - this.activeCodeWatchers.splice(index, 1); - } - return undefined; - } -} diff --git a/src/client/datascience/editor-integration/codewatcher.ts b/src/client/datascience/editor-integration/codewatcher.ts deleted file mode 100644 index 7def5654ad63..000000000000 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { inject, injectable } from 'inversify'; -import { CodeLens, Command, Position, Range, Selection, TextDocument, TextEditorRevealType } from 'vscode'; - -import { IApplicationShell, IDocumentManager } from '../../common/application/types'; -import { ILogger } from '../../common/types'; -import * as localize from '../../common/utils/localize'; -import { captureTelemetry } from '../../telemetry'; -import { generateCellRanges } from '../cellFactory'; -import { Commands, Telemetry } from '../constants'; -import { JupyterInstallError } from '../jupyter/jupyterInstallError'; -import { ICodeWatcher, IHistoryProvider } from '../types'; - -@injectable() -export class CodeWatcher implements ICodeWatcher { - private document?: TextDocument; - private version: number = -1; - private fileName: string = ''; - private codeLenses: CodeLens[] = []; - - constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(ILogger) private logger: ILogger, - @inject(IHistoryProvider) private historyProvider : IHistoryProvider, - @inject(IDocumentManager) private documentManager : IDocumentManager) {} - - public addFile(document: TextDocument) { - this.document = document; - - // Cache these, we don't want to pull an old version if the document is updated - this.fileName = document.fileName; - this.version = document.version; - - // Get document cells here - const cells = generateCellRanges(document); - - this.codeLenses = []; - cells.forEach(cell => { - const cmd: Command = { - arguments: [this, cell.range], - title: localize.DataScience.runCellLensCommandTitle(), - command: Commands.RunCell - }; - this.codeLenses.push(new CodeLens(cell.range, cmd)); - const runAllCmd: Command = { - arguments: [this], - title: localize.DataScience.runAllCellsLensCommandTitle(), - command: Commands.RunAllCells - }; - this.codeLenses.push(new CodeLens(cell.range, runAllCmd)); - }); - } - - public getFileName() { - return this.fileName; - } - - public getVersion() { - return this.version; - } - - public getCodeLenses() { - return this.codeLenses; - } - - @captureTelemetry(Telemetry.RunAllCells) - public async runAllCells() { - const activeHistory = this.historyProvider.getOrCreateActive(); - - // 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 this.codeLenses) { - // Make sure that we have the correct command (RunCell) lenses - if (lens.command && lens.command.command === Commands.RunCell && lens.command.arguments && lens.command.arguments.length >= 2) { - const range: Range = lens.command.arguments[1]; - if (this.document && range) { - const code = this.document.getText(range); - await activeHistory.addCode(code, this.getFileName(), range.start.line); - } - } - } - - // If there are no codelenses, just run all of the code as a single cell - if (this.codeLenses.length === 0) { - if (this.document) { - const code = this.document.getText(); - await activeHistory.addCode(code, this.getFileName(), 0); - } - } - } - - @captureTelemetry(Telemetry.RunCell) - public async runCell(range: Range) { - const activeHistory = this.historyProvider.getOrCreateActive(); - if (this.document) { - const code = this.document.getText(range); - - try { - await activeHistory.addCode(code, this.getFileName(), range.start.line, this.documentManager.activeTextEditor); - } catch (err) { - this.handleError(err); - } - - } - } - - @captureTelemetry(Telemetry.RunCurrentCell) - public async runCurrentCell() { - if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { - return; - } - - for (const lens of this.codeLenses) { - // Check to see which RunCell lens range overlaps the current selection start - if (lens.range.contains(this.documentManager.activeTextEditor.selection.start) && lens.command && lens.command.command === Commands.RunCell) { - await this.runCell(lens.range); - break; - } - } - } - - @captureTelemetry(Telemetry.RunCurrentCellAndAdvance) - public async runCurrentCellAndAdvance() { - if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { - return; - } - - let currentRunCellLens: CodeLens | undefined; - let nextRunCellLens: CodeLens | undefined; - - for (const lens of this.codeLenses) { - // If we have already found the current code lens, then the next run cell code lens will give us the next cell - if (currentRunCellLens && lens.command && lens.command.command === Commands.RunCell) { - nextRunCellLens = lens; - break; - } - - // Check to see which RunCell lens range overlaps the current selection start - if (lens.range.contains(this.documentManager.activeTextEditor.selection.start) && lens.command && lens.command.command === Commands.RunCell) { - currentRunCellLens = lens; - } - } - - if (currentRunCellLens) { - // Either use the next cell that we found, or add a new one into the document - let nextRange: Range; - if (!nextRunCellLens) { - nextRange = this.createNewCell(currentRunCellLens.range); - } else { - nextRange = nextRunCellLens.range; - } - - if (nextRange) { - this.advanceToRange(nextRange); - } - - // Run the cell after moving the selection - await this.runCell(currentRunCellLens.range); - } - } - - // tslint:disable-next-line:no-any - private handleError = (err : any) => { - if (err instanceof JupyterInstallError) { - const jupyterError = err as JupyterInstallError; - - // This is a special error that shows a link to open for more help - this.applicationShell.showErrorMessage(jupyterError.message, jupyterError.actionTitle).then(v => { - // User clicked on the link, open it. - if (v === jupyterError.actionTitle) { - this.applicationShell.openUrl(jupyterError.action); - } - }); - } else if (err.message) { - this.applicationShell.showErrorMessage(err.message); - } else { - this.applicationShell.showErrorMessage(err.toString()); - } - this.logger.logError(err); - } - - // User has picked run and advance on the last cell of a document - // Create a new cell at the bottom and put their selection there, ready to type - private createNewCell(currentRange: Range): Range { - const editor = this.documentManager.activeTextEditor; - const newPosition = new Position(currentRange.end.line + 3, 0); // +3 to account for the added spaces and to position after the new mark - - if (editor) { - editor.edit((editBuilder) => { - editBuilder.insert(new Position(currentRange.end.line + 1, 0), '\n\n#%%\n'); - }); - } - - return new Range(newPosition, newPosition); - } - - // 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/history.ts b/src/client/datascience/history.ts deleted file mode 100644 index ece5d4ab81d7..000000000000 --- a/src/client/datascience/history.ts +++ /dev/null @@ -1,777 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../common/extensions'; - -import { nbformat } from '@jupyterlab/coreutils'; -import * as fs from 'fs-extra'; -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import * as uuid from 'uuid/v4'; -import { Event, EventEmitter, Position, Range, Selection, TextEditor, Uri, ViewColumn } from 'vscode'; -import { Disposable } from 'vscode-jsonrpc'; - -import { - IApplicationShell, - ICommandManager, - IDocumentManager, - IWebPanel, - IWebPanelMessageListener, - IWebPanelProvider, - IWorkspaceService -} from '../common/application/types'; -import { CancellationError } from '../common/cancellation'; -import { EXTENSION_ROOT_DIR } from '../common/constants'; -import { ContextKey } from '../common/contextKey'; -import { IFileSystem } from '../common/platform/types'; -import { IConfigurationService, IDisposableRegistry, ILogger } from '../common/types'; -import { createDeferred } from '../common/utils/async'; -import * as localize from '../common/utils/localize'; -import { IInterpreterService } from '../interpreter/contracts'; -import { captureTelemetry, sendTelemetryEvent } from '../telemetry'; -import { EditorContexts, HistoryMessages, Settings, Telemetry } from './constants'; -import { JupyterInstallError } from './jupyter/jupyterInstallError'; -import { - CellState, - ICell, - ICodeCssGenerator, - IConnection, - IHistory, - IHistoryInfo, - IJupyterExecution, - INotebookExporter, - INotebookServer, - InterruptResult, - IStatusProvider -} from './types'; - -export enum SysInfoReason { - Start, - Restart, - Interrupt -} - -@injectable() -export class History implements IWebPanelMessageListener, IHistory { - private disposed : boolean = false; - private webPanel : IWebPanel | undefined; - private loadPromise: Promise<void>; - private settingsChangedDisposable : Disposable; - private closedEvent : EventEmitter<IHistory>; - private unfinishedCells: ICell[] = []; - private restartingKernel: boolean = false; - private potentiallyUnfinishedStatus: Disposable[] = []; - private addedSysInfo: boolean = false; - private ignoreCount: number = 0; - private waitingForExportCells : boolean = false; - private jupyterServer: INotebookServer | undefined; - - constructor( - @inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(IDocumentManager) private documentManager: IDocumentManager, - @inject(IInterpreterService) private interpreterService: IInterpreterService, - @inject(IWebPanelProvider) private provider: IWebPanelProvider, - @inject(IDisposableRegistry) private disposables: IDisposableRegistry, - @inject(ICodeCssGenerator) private cssGenerator : ICodeCssGenerator, - @inject(ILogger) private logger : ILogger, - @inject(IStatusProvider) private statusProvider : IStatusProvider, - @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, - @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(IConfigurationService) private configuration: IConfigurationService, - @inject(ICommandManager) private commandManager: ICommandManager, - @inject(INotebookExporter) private jupyterExporter: INotebookExporter, - @inject(IWorkspaceService) private workspaceService: IWorkspaceService) { - - // Sign up for configuration changes - this.settingsChangedDisposable = this.interpreterService.onDidChangeInterpreter(this.onSettingsChanged); - - // Create our event emitter - this.closedEvent = new EventEmitter<IHistory>(); - this.disposables.push(this.closedEvent); - - // Load on a background thread. - this.loadPromise = this.load(); - } - - public async show() : Promise<void> { - if (!this.disposed) { - // Make sure we're loaded first - await this.loadPromise; - - // Then show our web panel. - if (this.webPanel && this.jupyterServer) { - await this.webPanel.show(); - } - } - } - - public get closed() : Event<IHistory> { - return this.closedEvent.event; - } - - public async addCode(code: string, file: string, line: number, editor?: TextEditor) : Promise<void> { - // Start a status item - const status = this.setStatus(localize.DataScience.executingCode()); - - // Create a deferred object that will wait until the status is disposed - const finishedAddingCode = createDeferred<void>(); - const actualDispose = status.dispose; - status.dispose = () => { - finishedAddingCode.resolve(); - actualDispose(); - }; - - try { - - // Make sure we're loaded first. - const statusLoad = this.setStatus(localize.DataScience.startingJupyter()); - try { - await this.loadPromise; - } finally { - statusLoad.dispose(); - } - - // Then show our webpanel - await this.show(); - - // Add our sys info if necessary - await this.addSysInfo(SysInfoReason.Start); - - if (this.jupyterServer) { - // 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.jupyterServer.setInitialDirectory(path.dirname(file)); - - // Attempt to evaluate this cell in the jupyter notebook - const observable = this.jupyterServer.executeObservable(code, file, line); - - // Sign up for cell changes - observable.subscribe( - (cells: ICell[]) => { - this.onAddCodeEvent(cells, editor); - }, - (error) => { - status.dispose(); - if (!(error instanceof CancellationError)) { - this.applicationShell.showErrorMessage(error); - } - }, - () => { - // Indicate executing until this cell is done. - status.dispose(); - }); - - // Wait for the cell to finish - await finishedAddingCode.promise; - } - } catch (err) { - status.dispose(); - - // We failed, dispose of ourselves too so that nobody uses us again - this.dispose().ignoreErrors(); - - throw err; - } - } - - // tslint:disable-next-line: no-any no-empty - public postMessage(type: string, payload?: any) { - if (this.webPanel) { - this.webPanel.postMessage({type: type, payload: payload}); - } - } - - // tslint:disable-next-line: no-any no-empty - public onMessage = (message: string, payload: any) => { - switch (message) { - case HistoryMessages.GotoCodeCell: - this.gotoCode(payload.file, payload.line); - break; - - case HistoryMessages.RestartKernel: - this.restartKernel(); - break; - - case HistoryMessages.ReturnAllCells: - this.handleReturnAllCells(payload); - break; - - case HistoryMessages.Interrupt: - this.interruptKernel(); - break; - - case HistoryMessages.Export: - this.export(payload); - break; - - case HistoryMessages.SendInfo: - this.updateContexts(payload); - break; - - case HistoryMessages.DeleteAllCells: - this.logTelemetry(Telemetry.DeleteAllCells); - break; - - case HistoryMessages.DeleteCell: - this.logTelemetry(Telemetry.DeleteCell); - break; - - case HistoryMessages.Undo: - this.logTelemetry(Telemetry.Undo); - break; - - case HistoryMessages.Redo: - this.logTelemetry(Telemetry.Redo); - break; - - case HistoryMessages.ExpandAll: - this.logTelemetry(Telemetry.ExpandAll); - break; - - case HistoryMessages.CollapseAll: - this.logTelemetry(Telemetry.CollapseAll); - break; - - default: - break; - } - } - - public async dispose() { - if (!this.disposed) { - this.disposed = true; - this.settingsChangedDisposable.dispose(); - this.closedEvent.fire(this); - if (this.jupyterServer) { - await this.jupyterServer.shutdown(); - } - this.updateContexts(); - } - } - - @captureTelemetry(Telemetry.Undo) - public undoCells() { - this.postMessage(HistoryMessages.Undo); - } - - @captureTelemetry(Telemetry.Redo) - public redoCells() { - this.postMessage(HistoryMessages.Redo); - } - - @captureTelemetry(Telemetry.DeleteAllCells) - public removeAllCells() { - this.postMessage(HistoryMessages.DeleteAllCells); - } - - @captureTelemetry(Telemetry.ExpandAll) - public expandAllCells() { - this.postMessage(HistoryMessages.ExpandAll); - } - - @captureTelemetry(Telemetry.CollapseAll) - public collapseAllCells() { - this.postMessage(HistoryMessages.CollapseAll); - } - - 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(HistoryMessages.GetAllCells); - } - - @captureTelemetry(Telemetry.RestartKernel) - public restartKernel() { - if (this.jupyterServer && !this.restartingKernel) { - // Ask the user if they want us to restart or not. - const message = localize.DataScience.restartKernelMessage(); - const yes = localize.DataScience.restartKernelMessageYes(); - const no = localize.DataScience.restartKernelMessageNo(); - - this.applicationShell.showInformationMessage(message, yes, no).then(v => { - if (v === yes) { - this.restartKernelInternal().catch(e => { - this.applicationShell.showErrorMessage(e); - this.logger.logError(e); - }); - } - }); - } - } - - @captureTelemetry(Telemetry.Interrupt) - public interruptKernel() { - if (this.jupyterServer && !this.restartingKernel) { - const status = this.statusProvider.set(localize.DataScience.interruptKernelStatus()); - - const settings = this.configuration.getSettings(); - const interruptTimeout = settings.datascience.jupyterInterruptTimeout; - - this.jupyterServer.interruptKernel(interruptTimeout) - .then(result => { - status.dispose(); - if (result === InterruptResult.TimedOut) { - const message = localize.DataScience.restartKernelAfterInterruptMessage(); - const yes = localize.DataScience.restartKernelMessageYes(); - const no = localize.DataScience.restartKernelMessageNo(); - - this.applicationShell.showInformationMessage(message, yes, no).then(v => { - if (v === yes) { - this.restartKernelInternal().catch(e => { - this.applicationShell.showErrorMessage(e); - this.logger.logError(e); - }); - } - }); - } else if (result === InterruptResult.Restarted) { - // Uh-oh, keyboard interrupt crashed the kernel. - this.addSysInfo(SysInfoReason.Interrupt).ignoreErrors(); - } - }) - .catch(err => { - status.dispose(); - this.logger.logError(err); - this.applicationShell.showErrorMessage(err); - }); - } - } - - private async restartKernelInternal() : Promise<void> { - this.restartingKernel = true; - - // First we need to finish all outstanding cells. - this.unfinishedCells.forEach(c => { - c.state = CellState.error; - if (this.webPanel) { - this.webPanel.postMessage({ type: HistoryMessages.FinishCell, payload: c }); - } - }); - this.unfinishedCells = []; - this.potentiallyUnfinishedStatus.forEach(s => s.dispose()); - this.potentiallyUnfinishedStatus = []; - - // Set our status - const status = this.statusProvider.set(localize.DataScience.restartingKernelStatus()); - - try { - if (this.jupyterServer) { - await this.jupyterServer.restartKernel(); - await this.addSysInfo(SysInfoReason.Restart); - } - } finally { - status.dispose(); - this.restartingKernel = false; - } - } - - // tslint:disable-next-line:no-any - private handleReturnAllCells = (payload: any) => { - // See what we're waiting for. - if (this.waitingForExportCells) { - this.export(payload); - } - } - - // tslint:disable-next-line:no-any - private updateContexts = (payload?: any) => { - // This should be called by the python interactive window every - // time state changes. We use this opportunity to update our - // extension contexts - const interactiveContext = new ContextKey(EditorContexts.HaveInteractive, this.commandManager); - interactiveContext.set(!this.disposed).catch(); - const interactiveCellsContext = new ContextKey(EditorContexts.HaveInteractiveCells, this.commandManager); - const redoableContext = new ContextKey(EditorContexts.HaveRedoableCells, this.commandManager); - if (payload && payload.info) { - const info = payload.info as IHistoryInfo; - if (info) { - interactiveCellsContext.set(info.cellCount > 0).catch(); - redoableContext.set(info.redoCount > 0).catch(); - } else { - interactiveCellsContext.set(false).catch(); - redoableContext.set(false).catch(); - } - } else { - interactiveCellsContext.set(false).catch(); - redoableContext.set(false).catch(); - } - } - - private setStatus = (message: string) : Disposable => { - const result = this.statusProvider.set(message); - this.potentiallyUnfinishedStatus.push(result); - return result; - } - - private logTelemetry = (event : string) => { - sendTelemetryEvent(event); - } - - private sendCell(cell: ICell, message: string) { - // Remove our ignore count from the execution count prior to sending - const copy = JSON.parse(JSON.stringify(cell)); - if (copy.data && copy.data.execution_count !== null && copy.data.execution_count > 0) { - const count = cell.data.execution_count as number; - copy.data.execution_count = count - this.ignoreCount; - } - if (this.webPanel) { - this.webPanel.postMessage({type: message, payload: copy}); - } - } - - private onAddCodeEvent = (cells : ICell[], editor?: TextEditor) => { - // Send each cell to the other side - cells.forEach((cell : ICell) => { - if (this.webPanel) { - switch (cell.state) { - case CellState.init: - // Tell the react controls we have a new cell - this.sendCell(cell, HistoryMessages.StartCell); - - // 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.sendCell(cell, HistoryMessages.UpdateCell); - break; - - case CellState.error: - case CellState.finished: - // Tell the react controls we're done - this.sendCell(cell, HistoryMessages.FinishCell); - - // 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 - } - } - }); - - // If we have more than one cell, the second one should be a code cell. After it finishes, we need to inject a new cell entry - if (cells.length > 1 && cells[1].state === CellState.finished) { - // If we have an active editor, do the edit there so that the user can undo it, otherwise don't bother - if (editor) { - editor.edit((editBuilder) => { - editBuilder.insert(new Position(cells[1].line, 0), '#%%\n'); - }); - } - } - } - - private onSettingsChanged = async () => { - // Update our load promise. We need to restart the jupyter server - if (this.loadPromise) { - await this.loadPromise; - if (this.jupyterServer) { - await this.jupyterServer.shutdown(); - } - } - this.loadPromise = this.loadJupyterServer(true); - } - - @captureTelemetry(Telemetry.GotoSourceCode, {}, false) - private gotoCode(file: string, line: number) { - this.gotoCodeInternal(file, line).catch(err => { - this.applicationShell.showErrorMessage(err); - }); - } - - private async gotoCodeInternal(file: string, line: number) { - let editor : TextEditor | undefined; - - if (await fs.pathExists(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)); - } - } - - @captureTelemetry(Telemetry.ExportNotebook, {}, false) - // tslint:disable-next-line: no-any no-empty - private export (payload: any) { - if (payload.contents) { - // Should be an array of cells - const cells = payload.contents as ICell[]; - if (cells && this.applicationShell) { - - const filtersKey = localize.DataScience.exportDialogFilter(); - const filtersObject = {}; - filtersObject[filtersKey] = ['ipynb']; - - // Bring up the open file dialog box - this.applicationShell.showSaveDialog( - { - saveLabel: localize.DataScience.exportDialogTitle(), - filters: filtersObject - }).then(async (uri: Uri | undefined) => { - if (uri) { - await this.exportToFile(cells, uri.fsPath); - } - }); - } - } - } - - private exportToFile = async (cells: ICell[], file : string) => { - // Take the list of cells, convert them to a notebook json format and write to disk - if (this.jupyterServer) { - let directoryChange; - const settings = this.configuration.getSettings(); - if (settings.datascience.changeDirOnImportExport) { - directoryChange = file; - } - - const notebook = await this.jupyterExporter.translateToNotebook(cells, directoryChange); - - try { - // tslint:disable-next-line: no-any - await this.fileSystem.writeFile(file, JSON.stringify(notebook), {encoding: 'utf8', flag: 'w'}); - this.applicationShell.showInformationMessage(localize.DataScience.exportDialogComplete().format(file), localize.DataScience.exportOpenQuestion()).then((str : string | undefined) => { - if (str && this.jupyterServer) { - // If the user wants to, open the notebook they just generated. - this.jupyterExecution.spawnNotebook(file).ignoreErrors(); - } - }); - } catch (exc) { - this.logger.logError('Error in exporting notebook file'); - this.applicationShell.showInformationMessage(localize.DataScience.exportDialogFailed().format(exc)); - } - } - } - - private loadJupyterServer = async (restart?: boolean) : Promise<void> => { - // Startup our jupyter server - const settings = this.configuration.getSettings(); - let serverURI: string | undefined = settings.datascience.jupyterServerURI; - let workingDir: string | undefined; - const useDefaultConfig : boolean | undefined = settings.datascience.useDefaultConfigForJupyter; - const status = this.setStatus(localize.DataScience.connectingToJupyter()); - try { - // For the local case pass in our URI as undefined, that way connect doesn't have to check the setting - if (serverURI === Settings.JupyterServerLocalLaunch) { - serverURI = undefined; - - workingDir = await this.calculateWorkingDirectory(); - } - this.jupyterServer = await this.jupyterExecution.connectToNotebookServer(serverURI, useDefaultConfig, undefined, workingDir); - - // If this is a restart, show our restart info - if (restart) { - await this.addSysInfo(SysInfoReason.Restart); - } - } finally { - if (status) { - status.dispose(); - } - } - } - - // Calculate the working directory that we should move into when starting up our Jupyter server locally - private calculateWorkingDirectory = async (): Promise<string | undefined> => - { - let workingDir: string | undefined; - // For a local launch calculate the working directory that we should switch into - const settings = this.configuration.getSettings(); - 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 && this.workspaceService.hasWorkspaceFolders) { - const workspaceFolderPath = this.workspaceService.workspaceFolders![0].uri.fsPath; - if (path.isAbsolute(fileRoot)) { - if (await this.fileSystem.directoryExists(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 { - // fileRoot is a relative path, combine it with the workspace folder - const combinedPath = path.join(workspaceFolderPath, fileRoot); - if (await this.fileSystem.directoryExists(combinedPath)) { - // combined path exists, use it - workingDir = combinedPath; - } else { - // Combined path doesn't exist, use workspace - workingDir = workspaceFolderPath; - } - } - } - return workingDir; - } - - 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(stream.text.toString()); - } else { - const data = o.data; - if (data && data.hasOwnProperty('text/plain')) { - result = result.concat(data['text/plain']); - } - } - }); - } - } - return result; - } - - private generateSysInfoCell = async (reason: SysInfoReason) : Promise<ICell | undefined> => { - // Execute the code 'import sys\r\nsys.version' and 'import sys\r\nsys.executable' to get our - // version and executable - if (this.jupyterServer) { - const message = await this.generateSysInfoMessage(reason); - // tslint:disable-next-line:no-multiline-string - const versionCells = await this.jupyterServer.execute(`import sys\r\nsys.version`, 'foo.py', 0); - // tslint:disable-next-line:no-multiline-string - const pathCells = await this.jupyterServer.execute(`import sys\r\nsys.executable`, 'foo.py', 0); - // tslint:disable-next-line:no-multiline-string - const notebookVersionCells = await this.jupyterServer.execute(`import notebook\r\nnotebook.version_info`, 'foo.py', 0); - - // 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() : ''; - - // Both should influence our ignore count. We don't want them to count against execution - this.ignoreCount = this.ignoreCount + 3; - - // Connection string only for our initial start, not restart or interrupt - let connectionString: string = ''; - if (reason === SysInfoReason.Start) { - connectionString = this.generateConnectionInfoString(this.jupyterServer.getConnectionInfo()); - } - - // Combine this data together to make our sys info - return { - data: { - cell_type: 'sys_info', - message: message, - version: version, - notebook_version: localize.DataScience.notebookVersionFormat().format(notebookVersion), - path: pythonPath, - connection: connectionString, - metadata: {}, - source: [] - }, - id: uuid(), - file: '', - line: 0, - state: CellState.finished - }; - } - } - - private async generateSysInfoMessage(reason: SysInfoReason): Promise<string> { - switch (reason) { - case SysInfoReason.Start: - // Message depends upon if ipykernel is supported or not. - if (!(await this.jupyterExecution.isKernelCreateSupported())) { - return localize.DataScience.pythonVersionHeaderNoPyKernel(); - } - return localize.DataScience.pythonVersionHeader(); - break; - case SysInfoReason.Restart: - return localize.DataScience.pythonRestartHeader(); - break; - case SysInfoReason.Interrupt: - return localize.DataScience.pythonInterruptFailedHeader(); - break; - default: - this.logger.logError('Invalid SysInfoReason'); - return ''; - break; - } - } - - private generateConnectionInfoString(connInfo: IConnection | undefined): string { - if (!connInfo) { - return ''; - } - - const tokenString = connInfo.token.length > 0 ? `?token=${connInfo.token}` : ''; - const urlString = `${connInfo.baseUrl}${tokenString}`; - - return `${localize.DataScience.sysInfoURILabel()}${urlString}`; - } - - private addSysInfo = async (reason: SysInfoReason) : Promise<void> => { - if (!this.addedSysInfo || reason === SysInfoReason.Interrupt || reason === SysInfoReason.Restart) { - this.addedSysInfo = true; - this.ignoreCount = 0; - - // Generate a new sys info cell and send it to the web panel. - const sysInfo = await this.generateSysInfoCell(reason); - if (sysInfo) { - this.onAddCodeEvent([sysInfo]); - } - } - } - - private loadWebPanel = async () : Promise<void> => { - // Create our web panel (it's the UI that shows up for the history) - - // Figure out the name of our main bundle. Should be in our output directory - const mainScriptPath = path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'history-react', 'index_bundle.js'); - - // Generate a css to put into the webpanel for viewing code - const css = await this.cssGenerator.generateThemeCss(); - - // Use this script to create our web view panel. It should contain all of the necessary - // script to communicate with this class. - this.webPanel = this.provider.create(this, localize.DataScience.historyTitle(), mainScriptPath, css); - } - - private load = async () : Promise<void> => { - const status = this.setStatus(localize.DataScience.startingJupyter()); - - // Check to see if we support ipykernel or not - try { - const usableInterpreter = await this.jupyterExecution.getUsableJupyterPython(); - if (!usableInterpreter) { - // Not loading anymore - status.dispose(); - - // Nobody is useable, throw an exception - throw new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink()); - } else { - // See if the usable interpreter is not our active one. If so, show a warning - const active = await this.interpreterService.getActiveInterpreter(); - const activeDisplayName = active ? active.displayName : undefined; - const activePath = active ? active.path : undefined; - const usableDisplayName = usableInterpreter ? usableInterpreter.displayName : undefined; - const usablePath = usableInterpreter ? usableInterpreter.path : undefined; - if (activePath && usablePath && !this.fileSystem.arePathsSame(activePath, usablePath) && activeDisplayName && usableDisplayName) { - this.applicationShell.showWarningMessage(localize.DataScience.jupyterKernelNotSupportedOnActive().format(activeDisplayName, usableDisplayName)); - } - } - - // Otherwise we continue loading - await Promise.all([this.loadJupyterServer(), this.loadWebPanel()]); - } finally { - status.dispose(); - } - } -} diff --git a/src/client/datascience/historyProvider.ts b/src/client/datascience/historyProvider.ts deleted file mode 100644 index 9f19b8e0b45c..000000000000 --- a/src/client/datascience/historyProvider.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 { IDisposableRegistry } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { IHistory, IHistoryProvider } from './types'; - -@injectable() -export class HistoryProvider implements IHistoryProvider { - - private activeHistory : IHistory | undefined; - - constructor( - @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IDisposableRegistry) private disposables: IDisposableRegistry) { - } - - public getActive() : IHistory | undefined { - return this.activeHistory; - } - - public getOrCreateActive() : IHistory { - if (!this.activeHistory) { - this.activeHistory = this.create(); - } - - return this.activeHistory; - } - - private create = () => { - const result = this.serviceContainer.get<IHistory>(IHistory); - const handler = result.closed(this.onHistoryClosed); - this.disposables.push(result); - this.disposables.push(handler); - return result; - } - - private onHistoryClosed = (history: IHistory) => { - if (this.activeHistory === history) { - this.activeHistory = undefined; - } - } - -} diff --git a/src/client/datascience/historycommandlistener.ts b/src/client/datascience/historycommandlistener.ts deleted file mode 100644 index 08fa3a3d17fc..000000000000 --- a/src/client/datascience/historycommandlistener.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 { inject, injectable } from 'inversify'; -import { Position, Range, TextDocument, Uri, ViewColumn } 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 { IFileSystem } from '../common/platform/types'; -import { IConfigurationService, IDisposableRegistry, ILogger } from '../common/types'; -import * as localize from '../common/utils/localize'; -import { captureTelemetry } from '../telemetry'; -import { CommandSource } from '../unittests/common/constants'; -import { generateCellRanges, generateCellsFromDocument } from './cellFactory'; -import { Commands, Telemetry } from './constants'; -import { - IDataScienceCommandListener, - IHistoryProvider, - IJupyterExecution, - INotebookExporter, - INotebookImporter, - INotebookServer, - IStatusProvider -} from './types'; - -@injectable() -export class HistoryCommandListener implements IDataScienceCommandListener { - constructor( - @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, - @inject(IHistoryProvider) private historyProvider: IHistoryProvider, - @inject(INotebookImporter) private jupyterImporter: INotebookImporter, - @inject(INotebookExporter) private jupyterExporter: INotebookExporter, - @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, - @inject(IDocumentManager) private documentManager: IDocumentManager, - @inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(ILogger) private logger: ILogger, - @inject(IConfigurationService) private configuration: IConfigurationService, - @inject(IStatusProvider) private statusProvider : IStatusProvider) - { - // Listen to document open commands. We want to ask the user if they want to import. - const disposable = this.documentManager.onDidOpenTextDocument(this.onOpenedDocument); - this.disposableRegistry.push(disposable); - } - - public register(commandManager: ICommandManager): void { - let disposable = commandManager.registerCommand(Commands.ShowHistoryPane, () => this.showHistoryPane()); - this.disposableRegistry.push(disposable); - disposable = commandManager.registerCommand(Commands.ImportNotebook, async (file: Uri, cmdSource: CommandSource = CommandSource.commandPalette) => { - await this.listenForErrors(async () => { - if (file && file.fsPath) { - await this.importNotebookOnFile(file.fsPath); - } else { - await this.importNotebook(); - } - }); - }); - this.disposableRegistry.push(disposable); - disposable = commandManager.registerCommand(Commands.ExportFileAsNotebook, async (file: Uri, cmdSource: CommandSource = CommandSource.commandPalette) => { - await this.listenForErrors(async () => { - if (file && file.fsPath) { - await this.exportFile(file.fsPath); - } else { - const activeEditor = this.documentManager.activeTextEditor; - if (activeEditor && activeEditor.document.languageId === PYTHON_LANGUAGE) { - await this.exportFile(activeEditor.document.fileName); - } - } - }); - }); - this.disposableRegistry.push(disposable); - disposable = commandManager.registerCommand(Commands.ExportFileAndOutputAsNotebook, async (file: Uri, cmdSource: CommandSource = CommandSource.commandPalette) => { - await this.listenForErrors(async () => { - if (file && file.fsPath) { - await this.exportFileAndOutput(file.fsPath); - } else { - const activeEditor = this.documentManager.activeTextEditor; - if (activeEditor && activeEditor.document.languageId === PYTHON_LANGUAGE) { - await this.exportFileAndOutput(activeEditor.document.fileName); - } - } - }); - }); - 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())); - } - - private async listenForErrors(promise: () => Promise<void>) : Promise<void> { - try { - await promise(); - } catch (err) { - if (!(err instanceof CancellationError)) { - if (err.message) { - this.logger.logError(err.message); - this.applicationShell.showErrorMessage(err.message); - } else { - this.logger.logError(err.toString()); - this.applicationShell.showErrorMessage(err.toString()); - } - } else { - this.logger.logInformation('Canceled'); - } - } - } - - @captureTelemetry(Telemetry.ExportPythonFile, {}, false) - private async exportFile(file: string): Promise<void> { - if (file && file.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.fileName, file)) { - const cells = generateCellsFromDocument(activeEditor.document); - if (cells) { - const filtersKey = localize.DataScience.exportDialogFilter(); - const filtersObject = {}; - 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) { - const notebook = await this.jupyterExporter.translateToNotebook(cells); - await this.fileSystem.writeFile(uri.fsPath, JSON.stringify(notebook)); - } - }, localize.DataScience.exportingFormat(), file); - - // When all done, show a notice that it completed. - const openQuestion = localize.DataScience.exportOpenQuestion(); - if (uri && uri.fsPath) { - this.applicationShell.showInformationMessage(localize.DataScience.exportDialogComplete().format(uri.fsPath), openQuestion).then((str: string | undefined) => { - if (str === openQuestion) { - // If the user wants to, open the notebook they just generated. - this.jupyterExecution.spawnNotebook(uri.fsPath).ignoreErrors(); - } - }); - } - } - } - } - } - - @captureTelemetry(Telemetry.ExportPythonFileAndOutput, {}, false) - private async exportFileAndOutput(file: string): Promise<void> { - if (file && file.length > 0 && this.jupyterExecution.isNotebookSupported()) { - // 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.fileName, file)) { - const ranges = generateCellRanges(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.applicationShell.showInformationMessage(localize.DataScience.exportDialogFailed().format(err)); - } - } - return Promise.resolve(); - }, localize.DataScience.exportingFormat(), file, () => { - cancelSource.cancel(); - }); - - // When all done, show a notice that it completed. - const openQuestion = localize.DataScience.exportOpenQuestion(); - this.applicationShell.showInformationMessage(localize.DataScience.exportDialogComplete().format(output), openQuestion).then((str: string | undefined) => { - if (str === openQuestion && output) { - // If the user wants to, open the notebook they just generated. - this.jupyterExecution.spawnNotebook(output).ignoreErrors(); - } - }); - - } - } - } - } else { - this.applicationShell.showErrorMessage(localize.DataScience.jupyterNotSupported()); - } - } - - private async exportCellsWithOutput(ranges: {range: Range; title: string}[], document: TextDocument, file: string, cancelToken: CancellationToken) : Promise<void> { - let server: INotebookServer | undefined; - try { - const settings = this.configuration.getSettings(); - const useDefaultConfig : boolean | undefined = settings.datascience.useDefaultConfigForJupyter; - - // Try starting a server. - server = await this.jupyterExecution.connectToNotebookServer(undefined, useDefaultConfig, cancelToken); - - // 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 server ? server.execute(code, document.fileName, r.range.start.line, cancelToken) : []; - }))); - - // Then save them to the file - const notebook = await this.jupyterExporter.translateToNotebook(cells); - await this.fileSystem.writeFile(file, JSON.stringify(notebook)); - - } finally { - if (server) { - server.dispose(); - } - } - } - - private async showExportDialog() : Promise<string | undefined> { - const filtersKey = localize.DataScience.exportDialogFilter(); - const filtersObject = {}; - filtersObject[filtersKey] = ['ipynb']; - - // Bring up the save file dialog box - const uri = await this.applicationShell.showSaveDialog({ - saveLabel: localize.DataScience.exportDialogTitle(), - filters: filtersObject - }); - - return uri ? uri.fsPath : undefined; - } - - private undoCells() { - const history = this.historyProvider.getActive(); - if (history) { - history.undoCells(); - } - } - - private redoCells() { - const history = this.historyProvider.getActive(); - if (history) { - history.redoCells(); - } - } - - private removeAllCells() { - const history = this.historyProvider.getActive(); - if (history) { - history.removeAllCells(); - } - } - - private interruptKernel() { - const history = this.historyProvider.getActive(); - if (history) { - history.interruptKernel(); - } - } - - private restartKernel() { - const history = this.historyProvider.getActive(); - if (history) { - history.restartKernel(); - } - } - - private expandAllCells() { - const history = this.historyProvider.getActive(); - if (history) { - history.expandAllCells(); - } - } - - private collapseAllCells() { - const history = this.historyProvider.getActive(); - if (history) { - history.collapseAllCells(); - } - } - - private exportCells() { - const history = this.historyProvider.getActive(); - if (history) { - history.exportCells(); - } - } - - private canImportFromOpenedFile = () => { - const settings = this.configuration.getSettings(); - return settings && (!settings.datascience || settings.datascience.allowImportFromNotebook); - } - - private disableImportOnOpenedFile = () => { - const settings = this.configuration.getSettings(); - if (settings && settings.datascience) { - settings.datascience.allowImportFromNotebook = false; - } - } - - private onOpenedDocument = async (document: TextDocument) => { - if (document.fileName.endsWith('.ipynb') && this.canImportFromOpenedFile()) { - const yes = localize.DataScience.notebookCheckForImportYes(); - const no = localize.DataScience.notebookCheckForImportNo(); - const dontAskAgain = localize.DataScience.notebookCheckForImportDontAskAgain(); - - const answer = await this.applicationShell.showInformationMessage( - localize.DataScience.notebookCheckForImportTitle(), - yes, no, dontAskAgain); - - try { - if (answer === yes) { - await this.importNotebookOnFile(document.fileName); - } else if (answer === dontAskAgain) { - this.disableImportOnOpenedFile(); - } - } catch (err) { - this.applicationShell.showErrorMessage(err); - } - } - - } - - @captureTelemetry(Telemetry.ShowHistoryPane, {}, false) - private showHistoryPane() : Promise<void>{ - const active = this.historyProvider.getOrCreateActive(); - return active.show(); - } - - private waitForStatus<T>(promise: () => Promise<T>, format: string, file?: string, canceled?: () => void) : Promise<T> { - const message = file ? format.format(file) : format; - return this.statusProvider.waitWithStatus(promise, message, undefined, canceled); - } - - @captureTelemetry(Telemetry.ImportNotebook, { scope: 'command' }, false) - private async importNotebook() : Promise<void> { - const filtersKey = localize.DataScience.importDialogFilter(); - const filtersObject = {}; - 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.jupyterImporter.importFromFile(uris[0].fsPath); - await this.viewDocument(contents); - }, localize.DataScience.importingFormat(), uris[0].fsPath); - } - } - - @captureTelemetry(Telemetry.ImportNotebook, { scope: 'file' }, false) - private async importNotebookOnFile(file: string) : Promise<void> { - if (file && file.length > 0) { - await this.waitForStatus(async () => { - const contents = await this.jupyterImporter.importFromFile(file); - await this.viewDocument(contents); - }, localize.DataScience.importingFormat(), file); - } - } - - private viewDocument = async (contents: string) : Promise<void> => { - const doc = await this.documentManager.openTextDocument({language: 'python', content: contents}); - const editor = await this.documentManager.showTextDocument(doc, ViewColumn.One); - - // 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/jupyter/jupyterConnectError.ts b/src/client/datascience/jupyter/jupyterConnectError.ts deleted file mode 100644 index a024ebefd31f..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 661eb0d45937..000000000000 --- a/src/client/datascience/jupyter/jupyterConnection.ts +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as path from 'path'; -import { CancellationToken, Disposable } from 'vscode-jsonrpc'; - -import { CancellationError } from '../../common/cancellation'; -import { IFileSystem } from '../../common/platform/types'; -import { ObservableExecutionResult, Output } from '../../common/process/types'; -import { IConfigurationService, ILogger } from '../../common/types'; -import { createDeferred, Deferred } from '../../common/utils/async'; -import * as localize from '../../common/utils/localize'; -import { IServiceContainer } from '../../ioc/types'; -import { IConnection } from '../types'; -import { JupyterConnectError } from './jupyterConnectError'; - -const UrlPatternRegEx = /(https?:\/\/[^\s]+)/ ; -const HttpPattern = /https?:\/\//; - -export type JupyterServerInfo = { - base_url: string; - notebook_dir: string; - hostname: string; - password: boolean; - pid: number; - port: number; - secure: boolean; - token: string; - url: string; -}; - -class JupyterConnectionWaiter { - private startPromise: Deferred<IConnection>; - private launchTimeout: NodeJS.Timer; - private configService: IConfigurationService; - private logger: ILogger; - private fileSystem: IFileSystem; - private notebook_dir: string; - private getServerInfo : (cancelToken?: CancellationToken) => Promise<JupyterServerInfo[] | undefined>; - private createConnection : (b: string, t: string, p: Disposable) => IConnection; - private launchResult : ObservableExecutionResult<string>; - private cancelToken : CancellationToken | undefined; - private stderr: string[] = []; - - constructor( - launchResult : ObservableExecutionResult<string>, - notebookFile: string, - getServerInfo: (cancelToken?: CancellationToken) => Promise<JupyterServerInfo[] | undefined>, - createConnection: (b: string, t: string, p: Disposable) => IConnection, - serviceContainer: IServiceContainer, - cancelToken?: CancellationToken) { - this.configService = serviceContainer.get<IConfigurationService>(IConfigurationService); - this.logger = serviceContainer.get<ILogger>(ILogger); - this.fileSystem = serviceContainer.get<IFileSystem>(IFileSystem); - this.getServerInfo = getServerInfo; - this.createConnection = createConnection; - this.launchResult = launchResult; - this.cancelToken = cancelToken; - - // Cancel our start promise if a cancellation occurs - if (cancelToken) { - cancelToken.onCancellationRequested(() => this.startPromise.reject(new CancellationError())); - } - - // Compute our notebook dir - this.notebook_dir = path.dirname(notebookFile); - - // Setup our start promise - this.startPromise = createDeferred<IConnection>(); - - // We want to reject our Jupyter connection after a specific timeout - const settings = this.configService.getSettings(); - const jupyterLaunchTimeout = settings.datascience.jupyterLaunchTimeout; - - this.launchTimeout = setTimeout(() => { - this.launchTimedOut(); - }, jupyterLaunchTimeout); - - // Listen on stderr for its connection information - launchResult.out.subscribe((output : Output<string>) => { - if (output.source === 'stderr') { - this.stderr.push(output.out); - this.extractConnectionInformation(output.out); - } else { - this.output(output.out); - } - }); - } - - public waitForConnection() : Promise<IConnection> { - return this.startPromise.promise; - } - - // tslint:disable-next-line:no-any - private output = (data: any) => { - if (this.logger) { - this.logger.logInformation(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 && !this.startPromise.completed) { - const matchInfo = serverInfos.find(info => this.fileSystem.arePathsSame(this.notebook_dir, info.notebook_dir)); - if (matchInfo) { - const url = matchInfo.url; - const token = matchInfo.token; - this.resolveStartPromise(url, token); - } - } - - // 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) { - const urlMatch = UrlPatternRegEx.exec(data); - if (urlMatch && !this.startPromise.completed) { - // 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(urlMatch[0]); - } 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')}`); - } - } - - // tslint:disable-next-line:no-any - private extractConnectionInformation = (data: any) => { - this.output(data); - - const httpMatch = HttpPattern.exec(data); - - if (httpMatch && this.notebook_dir && 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); - }).ignoreErrors(); - } - - // 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) => { - clearTimeout(this.launchTimeout); - this.startPromise.resolve(this.createConnection(baseUrl, token, this.launchResult)); - } - - // tslint:disable-next-line:no-any - private rejectStartPromise = (message: string) => { - clearTimeout(this.launchTimeout); - this.startPromise.reject(new JupyterConnectError(message, this.stderr.join('\n'))); - } - -} - -// Represents an active connection to a running jupyter notebook -export class JupyterConnection implements IConnection { - public baseUrl: string; - public token: string; - public localLaunch: boolean; - private disposable: Disposable | undefined; - constructor(baseUrl: string, token: string, disposable: Disposable) { - this.baseUrl = baseUrl; - this.token = token; - this.localLaunch = true; - this.disposable = disposable; - } - - public static waitForConnection( - notebookFile: string, - getServerInfo: (cancelToken?: CancellationToken) => Promise<JupyterServerInfo[] | undefined>, - notebookExecution : ObservableExecutionResult<string>, - serviceContainer: IServiceContainer, - cancelToken?: CancellationToken) { - - // Create our waiter. It will sit here and wait for the connection information from the jupyter process starting up. - const waiter = new JupyterConnectionWaiter( - notebookExecution, - notebookFile, - getServerInfo, - (baseUrl: string, token: string, processDisposable: Disposable) => new JupyterConnection(baseUrl, token, processDisposable), - serviceContainer, - cancelToken); - - return waiter.waitForConnection(); - } - - public dispose() { - if (this.disposable) { - this.disposable.dispose(); - this.disposable = undefined; - } - } -} diff --git a/src/client/datascience/jupyter/jupyterExecution.ts b/src/client/datascience/jupyter/jupyterExecution.ts deleted file mode 100644 index 866d7a1dd3ea..000000000000 --- a/src/client/datascience/jupyter/jupyterExecution.ts +++ /dev/null @@ -1,838 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { Kernel } from '@jupyterlab/services'; -import * as fs from 'fs-extra'; -import { inject, injectable } from 'inversify'; -import * as os from 'os'; -import * as path from 'path'; -import { URL } from 'url'; -import * as uuid from 'uuid/v4'; -import { CancellationToken, Disposable } from 'vscode-jsonrpc'; - -import { IWorkspaceService } from '../../common/application/types'; -import { Cancellation, CancellationError } from '../../common/cancellation'; -import { IS_WINDOWS } from '../../common/platform/constants'; -import { IFileSystem, TemporaryDirectory } from '../../common/platform/types'; -import { - ExecutionResult, - IProcessService, - IProcessServiceFactory, - IPythonExecutionFactory, - ObservableExecutionResult, - SpawnOptions -} from '../../common/process/types'; -import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, ILogger } from '../../common/types'; -import * as localize from '../../common/utils/localize'; -import { noop } from '../../common/utils/misc'; -import { EXTENSION_ROOT_DIR } from '../../constants'; -import { - ICondaService, - IInterpreterService, - IKnownSearchPathsForInterpreters, - InterpreterType, - PythonInterpreter -} from '../../interpreter/contracts'; -import { IServiceContainer } from '../../ioc/types'; -import { captureTelemetry } from '../../telemetry'; -import { Telemetry } from '../constants'; -import { IConnection, IJupyterExecution, IJupyterKernelSpec, IJupyterSessionManager, INotebookServer } from '../types'; -import { JupyterConnection, JupyterServerInfo } from './jupyterConnection'; -import { JupyterKernelSpec } from './jupyterKernelSpec'; - -const CheckJupyterRegEx = IS_WINDOWS ? /^jupyter?\.exe$/ : /^jupyter?$/; -const NotebookCommand = 'notebook'; -const ConvertCommand = 'nbconvert'; -const KernelSpecCommand = 'kernelspec'; -const KernelCreateCommand = 'ipykernel'; -const PyKernelOutputRegEx = /.*\s+(.+)$/m; -const KernelSpecOutputRegEx = /^\s*(\S+)\s+(\S+)$/; - -// JupyterCommand objects represent some process that can be launched that should be guaranteed to work because it -// was found by testing it previously -class JupyterCommand { - private exe: string; - private requiredArgs: string[]; - private launcher: IProcessService; - private interpreterPromise: Promise<PythonInterpreter | undefined>; - private condaService: ICondaService; - - constructor(exe: string, args: string[], launcher: IProcessService, interpreter: IInterpreterService | PythonInterpreter, condaService: ICondaService) { - this.exe = exe; - this.requiredArgs = args; - this.launcher = launcher; - this.condaService = condaService; - if (interpreter.hasOwnProperty('getInterpreterDetails')) { - const interpreterService = interpreter as IInterpreterService; - this.interpreterPromise = interpreterService.getInterpreterDetails(this.exe).catch(e => undefined); - } else { - const interpreterDetails = interpreter as PythonInterpreter; - this.interpreterPromise = Promise.resolve(interpreterDetails); - } - } - - public async mainVersion(): Promise<number> { - const interpreter = await this.interpreterPromise; - if (interpreter && interpreter.version) { - return interpreter.version.major; - } else { - return this.execVersion(); - } - } - - public interpreter() : Promise<PythonInterpreter | undefined> { - return this.interpreterPromise; - } - - public async execObservable(args: string[], options: SpawnOptions): Promise<ObservableExecutionResult<string>> { - const newOptions = { ...options }; - newOptions.env = await this.fixupCondaEnv(newOptions.env); - const newArgs = [...this.requiredArgs, ...args]; - return this.launcher.execObservable(this.exe, newArgs, newOptions); - } - - public async exec(args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> { - const newOptions = { ...options }; - newOptions.env = await this.fixupCondaEnv(newOptions.env); - const newArgs = [...this.requiredArgs, ...args]; - return this.launcher.exec(this.exe, newArgs, newOptions); - } - - /** - * Conda needs specific paths and env vars set to be happy. Call this function to fix up - * (or created if not present) our environment to run jupyter - */ - // Base Node.js SpawnOptions uses any for environment, so use that here as well - // tslint:disable-next-line:no-any - private async fixupCondaEnv(inputEnv?: NodeJS.ProcessEnv): Promise<any> { - if (!inputEnv) { - inputEnv = process.env; - } - const interpreter = await this.interpreterPromise; - - if (interpreter && interpreter.type === InterpreterType.Conda) { - return this.condaService.getActivatedCondaEnvironment(interpreter, inputEnv); - } - - return inputEnv; - } - - private async execVersion(): Promise<number> { - if (this.launcher) { - const output = await this.launcher.exec(this.exe, ['--version'], { throwOnStdErr: false, encoding: 'utf8' }); - // First number should be our result - const matches = /.*(\d+).*/m.exec(output.stdout); - if (matches && matches.length > 1) { - return parseInt(matches[1], 10); - } - } - return 0; - } - -} - -@injectable() -export class JupyterExecution implements IJupyterExecution, Disposable { - - private processServicePromise: Promise<IProcessService>; - private commands: { [command: string]: JupyterCommand } = {}; - private jupyterPath: string | undefined; - private usablePythonInterpreter: PythonInterpreter | undefined; - - constructor(@inject(IPythonExecutionFactory) private executionFactory: IPythonExecutionFactory, - @inject(ICondaService) private condaService: ICondaService, - @inject(IInterpreterService) private interpreterService: IInterpreterService, - @inject(IProcessServiceFactory) private processServiceFactory: IProcessServiceFactory, - @inject(IKnownSearchPathsForInterpreters) private knownSearchPaths: IKnownSearchPathsForInterpreters, - @inject(ILogger) private logger: ILogger, - @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, - @inject(IAsyncDisposableRegistry) private asyncRegistry: IAsyncDisposableRegistry, - @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(IJupyterSessionManager) private sessionManager: IJupyterSessionManager, - @inject(IWorkspaceService) workspace: IWorkspaceService, - @inject(IConfigurationService) private configuration: IConfigurationService, - @inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.processServicePromise = this.processServiceFactory.create(); - 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.dispose(); - } - }); - this.disposableRegistry.push(disposable); - } - } - - public dispose() { - // Clear our usableJupyterInterpreter - this.usablePythonInterpreter = undefined; - this.commands = {}; - } - - public isNotebookSupported(cancelToken?: CancellationToken): Promise<boolean> { - // See if we can find the command notebook - return Cancellation.race(() => this.isCommandSupported(NotebookCommand, cancelToken), cancelToken); - } - - public async getUsableJupyterPython(cancelToken?: CancellationToken): Promise<PythonInterpreter | undefined> { - // Only try to compute this once. - if (!this.usablePythonInterpreter) { - this.usablePythonInterpreter = await Cancellation.race(() => this.getUsableJupyterPythonImpl(cancelToken), cancelToken); - } - return this.usablePythonInterpreter; - } - - public isImportSupported = async (cancelToken?: CancellationToken): Promise<boolean> => { - // See if we can find the command nbconvert - return Cancellation.race(() => this.isCommandSupported(ConvertCommand), cancelToken); - } - - public isKernelCreateSupported = async (cancelToken?: CancellationToken): Promise<boolean> => { - // See if we can find the command ipykernel - return Cancellation.race(() => this.isCommandSupported(KernelCreateCommand), cancelToken); - } - - public isKernelSpecSupported = async (cancelToken?: CancellationToken): Promise<boolean> => { - // See if we can find the command kernelspec - return Cancellation.race(() => this.isCommandSupported(KernelSpecCommand), cancelToken); - } - - public connectToNotebookServer(uri: string | undefined, useDefaultConfig: boolean, cancelToken?: CancellationToken, workingDir?: string): Promise<INotebookServer | undefined> { - // Return nothing if we cancel - return Cancellation.race(async () => { - let connection: IConnection; - let kernelSpec: IJupyterKernelSpec | undefined; - - // If our uri is undefined or if it's set to local launch we need to launch a server locally - if (!uri) { - const launchResults = await this.startNotebookServer(useDefaultConfig, cancelToken); - if (launchResults) { - connection = launchResults.connection; - kernelSpec = launchResults.kernelSpec; - } 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 { - // If we have a URI spec up a connection info for it - connection = this.createRemoteConnectionInfo(uri); - kernelSpec = undefined; - } - - try { - // If we don't have a kernel spec yet, check using our current connection - if (!kernelSpec) { - kernelSpec = await this.getMatchingKernelSpec(connection, cancelToken); - } - - // If still not found, log an error (this seems possible for some people, so use the default) - if (!kernelSpec) { - this.logger.logError(localize.DataScience.jupyterKernelSpecNotFound()); - } - - // Try to connect to our jupyter process - const result = this.serviceContainer.get<INotebookServer>(INotebookServer); - await result.connect(connection, kernelSpec, cancelToken, workingDir); - return result; - } catch (err) { - // Something else went wrong - throw new Error(localize.DataScience.jupyterNotebookConnectFailed().format(connection.baseUrl, err)); - } - }, cancelToken); - } - - public spawnNotebook = async (file: string): Promise<void> => { - // First we find a way to start a notebook server - const notebookCommand = await this.findBestCommand('notebook'); - if (!notebookCommand) { - throw new Error(localize.DataScience.jupyterNotSupported()); - } - - const args: string[] = [`--NotebookApp.file_to_run=${file}`]; - - // Don't wait for the exec to finish and don't dispose. It's up to the user to kill the process - notebookCommand.exec(args, { throwOnStdErr: false, encoding: 'utf8' }).ignoreErrors(); - } - - public importNotebook = async (file: string, template: string): Promise<string> => { - // First we find a way to start a nbconvert - const convert = await this.findBestCommand(ConvertCommand); - if (!convert) { - throw new Error(localize.DataScience.jupyterNbConvertNotSupported()); - } - - // Wait for the nbconvert to finish - const result = await convert.exec([file, '--to', 'python', '--stdout', '--template', template], { throwOnStdErr: false, encoding: 'utf8' }); - if (result.stderr) { - // Stderr on nbconvert doesn't indicate failure. Just log the result - this.logger.logInformation(result.stderr); - } - return result.stdout; - } - - protected async getMatchingKernelSpec(connection?: IConnection, cancelToken?: CancellationToken): Promise<IJupyterKernelSpec | undefined> { - // If not using an active connection, check on disk - if (!connection) { - // Get our best interpreter. We want its python path - const bestInterpreter = await this.getUsableJupyterPython(cancelToken); - - // Enumerate our kernel specs that jupyter will know about and see if - // one of them already matches based on path - if (bestInterpreter && !await this.hasSpecPathMatch(bestInterpreter, cancelToken)) { - - // Nobody matches on path, so generate a new kernel spec - if (await this.isKernelCreateSupported(cancelToken)) { - await this.addMatchingSpec(bestInterpreter, cancelToken); - } - } - } - - // Now enumerate them again - const enumerator = connection ? () => this.sessionManager.getActiveKernelSpecs(connection) : () => this.enumerateSpecs(cancelToken); - - // Then find our match - return this.findSpecMatch(enumerator); - } - - private createRemoteConnectionInfo = (uri: string): IConnection => { - 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; - } - - return { - baseUrl: `${url.protocol}//${url.host}${url.pathname}`, - token: `${url.searchParams.get('token')}`, - localLaunch: false, - dispose: noop - }; - } - - @captureTelemetry(Telemetry.StartJupyter) - private async startNotebookServer(useDefaultConfig: boolean, cancelToken?: CancellationToken): Promise<{ connection: IConnection; kernelSpec: IJupyterKernelSpec | undefined }> { - // First we find a way to start a notebook server - const notebookCommand = await this.findBestCommand(NotebookCommand, cancelToken); - if (!notebookCommand) { - throw new Error(localize.DataScience.jupyterNotSupported()); - } - - // Now actually launch it - try { - // Generate a temp dir with a unique GUID, both to match up our started server and to easily clean up after - const tempDir = await this.generateTempDir(); - this.disposableRegistry.push(tempDir); - - // In the temp dir, create an empty config python file. This is the same - // as starting jupyter with all of the defaults. - const configFile = useDefaultConfig ? path.join(tempDir.path, 'jupyter_notebook_config.py') : undefined; - if (configFile) { - await this.fileSystem.writeFile(configFile, ''); - this.logger.logInformation(`Generating custom default config at ${configFile}`); - } - - // Create extra args based on if we have a config or not - const extraArgs: string[] = []; - if (useDefaultConfig) { - extraArgs.push(`--config=${configFile}`); - } - // 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. - if (process.env && process.env.VSCODE_PYTHON_DEBUG_JUPYTER) { - extraArgs.push('--debug'); - } - - // Use this temp file and config file to generate a list of args for our command - const args: string[] = [...['--no-browser', `--notebook-dir=${tempDir.path}`], ...extraArgs]; - - // Before starting the notebook process, make sure we generate a kernel spec - const kernelSpec = await this.getMatchingKernelSpec(undefined, cancelToken); - - // Then use this to launch our notebook process. - const launchResult = await notebookCommand.execObservable(args, { throwOnStdErr: false, encoding: 'utf8', token: cancelToken }); - - // 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 - const connection = await JupyterConnection.waitForConnection( - tempDir.path, this.getJupyterServerInfo, launchResult, this.serviceContainer, cancelToken); - - return { - connection: connection, - kernelSpec: kernelSpec - }; - } catch (err) { - if (err instanceof CancellationError) { - throw err; - } - - // Something else went wrong - throw new Error(localize.DataScience.jupyterNotebookFailure().format(err)); - } - } - - private getUsableJupyterPythonImpl = async (cancelToken?: CancellationToken): Promise<PythonInterpreter | undefined> => { - // This should be the best interpreter for notebooks - const found = await this.findBestCommand(NotebookCommand, cancelToken); - if (found) { - return found.interpreter(); - } - - return undefined; - } - - private getJupyterServerInfo = async (cancelToken?: CancellationToken): Promise<JupyterServerInfo[] | undefined> => { - // We have a small python file here that we will execute to get the server info from all running Jupyter instances - const bestInterpreter = await this.getUsableJupyterPython(cancelToken); - if (bestInterpreter) { - const newOptions: SpawnOptions = { mergeStdOutErr: true, token: cancelToken }; - newOptions.env = await this.fixupCondaEnv(newOptions.env, bestInterpreter); - const processService = await this.processServiceFactory.create(); - const file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getServerInfo.py'); - const serverInfoString = await processService.exec(bestInterpreter.path, [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) { - return undefined; - } - return serverInfos; - } - - return undefined; - } - - private onSettingsChanged() { - // Do the same thing as dispose so that we regenerate - // all of our commands - this.dispose(); - } - - private async addMatchingSpec(bestInterpreter: PythonInterpreter, cancelToken?: CancellationToken): Promise<void> { - const displayName = localize.DataScience.historyTitle(); - const ipykernelCommand = await this.findBestCommand(KernelCreateCommand, cancelToken); - - // If this fails, then we just skip this spec - try { - // Run the ipykernel install command. This will generate a new kernel spec. However - // it will be pointing to the python that ran it. We'll fix that up afterwards - const name = uuid(); - if (ipykernelCommand) { - const result = await ipykernelCommand.exec(['install', '--user', '--name', name, '--display-name', `'${displayName}'`], { throwOnStdErr: true, encoding: 'utf8', token: cancelToken }); - - // Result should have our file name. - const match = PyKernelOutputRegEx.exec(result.stdout); - const diskPath = match && match !== null && match.length > 1 ? path.join(match[1], 'kernel.json') : await this.findSpecPath(name); - - // Make sure we delete this file at some point. When we close VS code is probably good. It will also be destroy when - // the kernel spec goes away - this.asyncRegistry.push({ - dispose: async () => { - if (!diskPath) { - return; - } - try { - await fs.remove(path.dirname(diskPath)); - } catch { - noop(); - } - } - }); - - // If that works, rewrite our active interpreter into the argv - if (diskPath && bestInterpreter) { - if (await fs.pathExists(diskPath)) { - const specModel: Kernel.ISpecModel = await fs.readJSON(diskPath); - specModel.argv[0] = bestInterpreter.path; - await fs.writeJSON(diskPath, specModel, { flag: 'w', encoding: 'utf8' }); - } - } - } - } catch (err) { - this.logger.logError(err); - } - } - - private findSpecPath = async (specName: string, cancelToken?: CancellationToken): Promise<string | undefined> => { - // Enumerate all specs and get path for the match - const specs = await this.enumerateSpecs(cancelToken); - const match = specs.find(s => { - const js = s as JupyterKernelSpec; - return js && js.name === specName; - }) as JupyterKernelSpec; - return match ? match.specFile : undefined; - } - - private async generateTempDir(): Promise<TemporaryDirectory> { - const resultDir = path.join(os.tmpdir(), uuid()); - await this.fileSystem.createDirectory(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 fs.remove(resultDir); - count = 10; - } catch { - count += 1; - } - } - } - }; - } - - private isCommandSupported = async (command: string, cancelToken?: CancellationToken): Promise<boolean> => { - // See if we can find the command - try { - const result = await this.findBestCommand(command, cancelToken); - return result !== undefined; - } catch (err) { - this.logger.logWarning(err); - return false; - } - } - - /** - * Conda needs specific paths and env vars set to be happy. Call this function to fix up - * (or created if not present) our environment to run jupyter - */ - // Base Node.js SpawnOptions uses any for environment, so use that here as well - // tslint:disable-next-line:no-any - private fixupCondaEnv = async (inputEnv: NodeJS.ProcessEnv, interpreter: PythonInterpreter): Promise<any> => { - if (!inputEnv) { - inputEnv = process.env; - } - if (interpreter && interpreter.type === InterpreterType.Conda) { - return this.condaService.getActivatedCondaEnvironment(interpreter, inputEnv); - } - - return inputEnv; - } - - private hasSpecPathMatch = async (info: PythonInterpreter | undefined, cancelToken?: CancellationToken): Promise<boolean> => { - if (info) { - // Enumerate our specs - const specs = await this.enumerateSpecs(cancelToken); - - // See if any of their paths match - return specs.findIndex(s => { - if (info && s && s.path) { - return this.fileSystem.arePathsSame(s.path, info.path); - } - return false; - }) >= 0; - } - - // If no active interpreter, just act like everything is okay as we can't find a new spec anyway - return true; - } - - //tslint:disable-next-line:cyclomatic-complexity - private findSpecMatch = async (enumerator: () => Promise<(IJupyterKernelSpec | undefined)[]>): Promise<IJupyterKernelSpec | undefined> => { - // Extract our current python information that the user has picked. - // We'll match against this. - const info = await this.interpreterService.getActiveInterpreter(); - let bestScore = 0; - let bestSpec: IJupyterKernelSpec | undefined; - - // Then enumerate our specs - const specs = await enumerator(); - - // For each get its details as we will likely need them - const specDetails = await Promise.all(specs.map(async s => { - if (s && s.path && s.path.length > 0 && await fs.pathExists(s.path)) { - return this.interpreterService.getInterpreterDetails(s.path); - } - })); - - for (let i = 0; specs && i < specs.length; i += 1) { - const spec = specs[i]; - let score = 0; - - if (spec && spec.path && spec.path.length > 0 && info && spec.path === info.path) { - // Path match - score += 10; - } - if (spec && spec.language && spec.language.toLocaleLowerCase() === 'python') { - // Language match - score += 1; - - // See if the version is the same - if (info && info.version && specDetails[i]) { - const details = specDetails[i]; - if (details && details.version) { - if (details.version.major === info.version.major) { - // Major version match - score += 4; - - if (details.version.minor === info.version.minor) { - // Minor version match - score += 2; - - if (details.version.patch === info.version.patch) { - // Minor version match - score += 1; - } - } - } - } - } else if (info && info.version && spec && spec.path && spec.path.toLocaleLowerCase() === 'python' && spec.name) { - // This should be our current python. - - // 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[0], 10); - if (nameVersion && nameVersion === info.version.major) { - score += 4; - } - } - } - } - - // Update high score - if (score > bestScore) { - bestScore = score; - bestSpec = spec; - } - } - - // If still not set, at least pick the first one - if (!bestSpec && specs && specs.length > 0) { - bestSpec = specs[0]; - } - - return bestSpec; - } - - private async readSpec(kernelSpecOutputLine: string) : Promise<JupyterKernelSpec | undefined> { - const match = KernelSpecOutputRegEx.exec(kernelSpecOutputLine); - if (match && match !== null && match.length > 2) { - // Second match should be our path to the kernel spec - const file = path.join(match[2], 'kernel.json'); - if (await fs.pathExists(file)) { - // Turn this into a IJupyterKernelSpec - const model = await fs.readJSON(file, { encoding: 'utf8' }); - model.name = match[1]; - return new JupyterKernelSpec(model, file); - } - } - - return undefined; - } - - private enumerateSpecs = async (cancelToken?: CancellationToken): Promise<(IJupyterKernelSpec | undefined)[]> => { - if (await this.isKernelSpecSupported()) { - const kernelSpecCommand = await this.findBestCommand(KernelSpecCommand); - - if (kernelSpecCommand) { - try { - // Ask for our current list. - const list = await kernelSpecCommand.exec(['list'], { throwOnStdErr: true, encoding: 'utf8' }); - - // This should give us back a key value pair we can parse - const lines = list.stdout.splitLines({ trim: false, removeEmptyEntries: true }); - - // Generate all of the promises at once - const promises = lines.map(l => this.readSpec(l)); - - // Then let them run concurrently (they are file io) - const specs = await Promise.all(promises); - return specs.filter(s => s); - } catch { - // This is failing for some folks. In that case return nothing - return []; - } - } - } - - return []; - } - - private findInterpreterCommand = async (command: string, interpreter: PythonInterpreter, cancelToken?: CancellationToken): Promise<JupyterCommand | undefined> => { - // If the module is found on this interpreter, then we found it. - if (interpreter && await this.doesModuleExist(command, interpreter, cancelToken) && !Cancellation.isCanceled(cancelToken)) { - // We need a process service to create a command - const processService = await this.processServicePromise; - - // Our command args are different based on the command. ipykernel is not a jupyter command - const args = command === KernelCreateCommand ? ['-m', command] : ['-m', 'jupyter', command]; - - return new JupyterCommand(interpreter.path, args, processService, interpreter, this.condaService); - } - - return undefined; - } - - private lookForJupyterInDirectory = async (pathToCheck: string): Promise<string[]> => { - try { - const files = await this.fileSystem.getFiles(pathToCheck); - return files ? files.filter(s => CheckJupyterRegEx.test(path.basename(s))) : []; - } catch (err) { - this.logger.logWarning('Python Extension (fileSystem.getFiles):', err); - } - return [] as string[]; - } - - private searchPathsForJupyter = async (): Promise<string | undefined> => { - if (!this.jupyterPath) { - const paths = this.knownSearchPaths.getSearchPaths(); - for (let i = 0; i < paths.length && !this.jupyterPath; i += 1) { - const found = await this.lookForJupyterInDirectory(paths[i]); - if (found.length > 0) { - this.jupyterPath = found[0]; - } - } - } - return this.jupyterPath; - } - - private findPathCommand = async (command: string, cancelToken?: CancellationToken): Promise<JupyterCommand | undefined> => { - if (await this.doesJupyterCommandExist(command, cancelToken) && !Cancellation.isCanceled(cancelToken)) { - // Search the known paths for jupyter - const jupyterPath = await this.searchPathsForJupyter(); - if (jupyterPath) { - // We need a process service to create a command - const processService = await this.processServicePromise; - return new JupyterCommand(jupyterPath, [command], processService, this.interpreterService, this.condaService); - } - } - return undefined; - } - - private supportsSearchingForCommands() : boolean { - if (this.configuration) { - const settings = this.configuration.getSettings(); - if (settings) { - return settings.datascience.searchForJupyter; - } - } - return true; - } - - // For jupyter, - // - Look in current interpreter, if found create something that has path and args - // - Look in other interpreters, if found create something that has path and args - // - Look on path, if found create something that has path and args - // For general case - // - Look for module in current interpreter, if found create something with python path and -m module - // - Look in other interpreters, if found create something with python path and -m module - // - Look on path for jupyter, if found create something with jupyter path and args - private findBestCommand = async (command: string, cancelToken?: CancellationToken): Promise<JupyterCommand | undefined> => { - // See if we already have this command in list - if (!this.commands.hasOwnProperty(command)) { - // Not found, try to find it. - - // First we look in the current interpreter - const current = await this.interpreterService.getActiveInterpreter(); - let found = current ? await this.findInterpreterCommand(command, current, cancelToken) : undefined; - if (!found && this.supportsSearchingForCommands()) { - // Look through all of our interpreters (minus the active one at the same time) - const all = await this.interpreterService.getInterpreters(); - const promises = all.filter(i => i !== current).map(i => this.findInterpreterCommand(command, i, cancelToken)); - const foundList = await Promise.all(promises); - - // Then go through all of the found ones and pick the closest python match - if (current && current.version) { - let bestScore = -1; - for (let i = 0; i < foundList.length; i += 1) { - let currentScore = 0; - if (foundList[i]) { - const interpreter = await foundList[i].interpreter(); - const version = interpreter.version; - if (version) { - if (version.major === current.version.major) { - currentScore += 4; - if (version.minor === current.version.minor) { - currentScore += 2; - if (version.patch === current.version.patch) { - currentScore += 1; - } - } - } - } - if (currentScore > bestScore) { - found = foundList[i]; - bestScore = currentScore; - } - } - } - } else { - // Just pick the first one - found = foundList.find(f => f !== undefined); - } - } - - // If still not found, try looking on the path using jupyter - if (!found) { - found = await this.findPathCommand(command, cancelToken); - } - - // If we found a command, save in our dictionary - if (found) { - this.commands[command] = found; - } - } - - // Return result - return this.commands.hasOwnProperty(command) ? this.commands[command] : undefined; - } - - private doesModuleExist = async (module: string, interpreter: PythonInterpreter, cancelToken?: CancellationToken): Promise<boolean> => { - if (interpreter && interpreter !== null) { - const newOptions: SpawnOptions = { throwOnStdErr: true, encoding: 'utf8', token: cancelToken }; - newOptions.env = await this.fixupCondaEnv(newOptions.env, interpreter); - const pythonService = await this.executionFactory.create({ pythonPath: interpreter.path }); - try { - // Special case for ipykernel - const actualModule = module === KernelCreateCommand ? module : 'jupyter'; - const args = module === KernelCreateCommand ? ['--version'] : [module, '--version']; - - const result = await pythonService.execModule(actualModule, args, newOptions); - return !result.stderr; - } catch (err) { - this.logger.logWarning(`${err} for ${interpreter.path}`); - return false; - } - } else { - return false; - } - } - - private doesJupyterCommandExist = async (command?: string, cancelToken?: CancellationToken): Promise<boolean> => { - const newOptions: SpawnOptions = { throwOnStdErr: true, encoding: 'utf8', token: cancelToken }; - const args = command ? [command, '--version'] : ['--version']; - const processService = await this.processServicePromise; - try { - const result = await processService.exec('jupyter', args, newOptions); - return !result.stderr; - } catch (err) { - this.logger.logWarning(err); - return false; - } - } - -} diff --git a/src/client/datascience/jupyter/jupyterExporter.ts b/src/client/datascience/jupyter/jupyterExporter.ts deleted file mode 100644 index c5872d23a1b1..000000000000 --- a/src/client/datascience/jupyter/jupyterExporter.ts +++ /dev/null @@ -1,188 +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 * as uuid from 'uuid/v4'; - -import * as os from 'os'; -import * as path from 'path'; -import { IWorkspaceService } from '../../common/application/types'; -import { IFileSystem } from '../../common/platform/types'; -import { ILogger } from '../../common/types'; -import * as localize from '../../common/utils/localize'; -import { noop } from '../../common/utils/misc'; -import { CodeSnippits, RegExpValues } from '../constants'; -import { CellState, ICell, IJupyterExecution, INotebookExporter, ISysInfo } from '../types'; - -@injectable() -export class JupyterExporter implements INotebookExporter { - - constructor( - @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, - @inject(ILogger) private logger: ILogger, - @inject(IWorkspaceService) private workspaceService: IWorkspaceService, - @inject(IFileSystem) private fileSystem: IFileSystem) { - } - - public dispose() { - noop(); - } - - public async translateToNotebook(cells: ICell[], changeDirectory?: string): Promise<nbformat.INotebookContent | undefined> { - // If requested, add in a change directory cell to fix relative paths - if (changeDirectory) { - cells = await this.addDirectoryChangeCell(cells, changeDirectory); - } - - // First compute our python version number - const pythonNumber = await this.extractPythonMainVersion(cells); - - // Use this to build our metadata object - const metadata: nbformat.INotebookMetadata = { - language_info: { - name: 'python', - codemirror_mode: { - name: 'ipython', - version: pythonNumber - } - }, - orig_nbformat: 2, - file_extension: '.py', - mimetype: 'text/x-python', - name: 'python', - npconvert_exporter: 'python', - pygments_lexer: `ipython${pythonNumber}`, - version: pythonNumber - }; - - // Combine this into a JSON object - return { - cells: this.pruneCells(cells), - nbformat: 4, - nbformat_minor: 2, - metadata: metadata - }; - } - - // 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<ICell[]> => { - const changeDirectory = await this.calculateDirectoryChange(file, cells); - - if (changeDirectory) { - const exportChangeDirectory = CodeSnippits.ChangeDirectory.join(os.EOL).format(localize.DataScience.exportChangeDirectoryComment(), changeDirectory); - - const cell: ICell = { - data: { - source: exportChangeDirectory, - cell_type: 'code', - outputs: [], - metadata: {}, - execution_count: 0 - }, - id: uuid(), - file: '', - 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<string | undefined> => { - 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.fileExists(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<string | undefined> => { - let directoryChange: string | undefined; - 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)) { - return directoryChange; - } else { - return undefined; - } - } - - private pruneCells = (cells: ICell[]): nbformat.IBaseCell[] => { - // First filter out sys info cells. Jupyter doesn't understand these - return cells.filter(c => c.data.cell_type !== 'sys_info') - // Then prune each cell down to just the cell data. - .map(this.pruneCell); - } - - private pruneCell = (cell: ICell): 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); - return copy; - } - - private pruneSource = (source: nbformat.MultilineString): nbformat.MultilineString => { - - if (Array.isArray(source) && source.length > 0) { - if (RegExpValues.PythonCellMarker.test(source[0])) { - return source.slice(1); - } - } else { - const array = source.toString().split('\n').map(s => `${s}\n`); - if (array.length > 0 && RegExpValues.PythonCellMarker.test(array[0])) { - return array.slice(1); - } - } - - return source; - } - - private extractPythonMainVersion = async (cells: ICell[]): Promise<number> => { - let pythonVersion; - const sysInfoCells = cells.filter((targetCell: ICell) => { - return targetCell.data.cell_type === 'sys_info'; - }); - - if (sysInfoCells.length > 0) { - const sysInfo = sysInfoCells[0].data as ISysInfo; - const fullVersionString = sysInfo.version; - if (fullVersionString) { - pythonVersion = fullVersionString.substr(0, fullVersionString.indexOf('.')); - return Number(pythonVersion); - } - } - - this.logger.logInformation('Failed to find python main version from sys_info cell'); - - // In this case, let's check the version on 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 6cba06920b5e..000000000000 --- a/src/client/datascience/jupyter/jupyterImporter.ts +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as fs from 'fs-extra'; -import { inject, injectable } from 'inversify'; -import * as os from 'os'; -import * as path from 'path'; -import { IWorkspaceService } from '../../common/application/types'; -import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry } from '../../common/types'; -import * as localize from '../../common/utils/localize'; -import { CodeSnippits } from '../constants'; -import { IJupyterExecution, 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 nbconvertTemplate = - // tslint:disable-next-line:no-multiline-string - `{%- extends 'null.tpl' -%} -{% block codecell %} -#%% -{{ super() }} -{% endblock codecell %} -{% block in_prompt %}{% endblock in_prompt %} -{% block input %}{{ cell.source | ipython2python }}{% endblock input %} -{% block markdowncell scoped %}#%% [markdown] -{{ cell.source | comment_lines }} -{% endblock markdowncell %}`; - - private templatePromise: Promise<string>; - - constructor( - @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, - @inject(IConfigurationService) private configuration: IConfigurationService, - @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, - @inject(IWorkspaceService) private workspaceService: IWorkspaceService) { - this.templatePromise = this.createTemplateFile(); - } - - public async importFromFile(file: string): Promise<string> { - 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 = this.calculateDirectoryChange(file); - } - - // Use the jupyter nbconvert functionality to turn the notebook into a python file - if (await this.jupyterExecution.isImportSupported()) { - const fileOutput: string = await this.jupyterExecution.importNotebook(file, template); - if (directoryChange) { - return this.addDirectoryChange(fileOutput, directoryChange); - } else { - return fileOutput; - } - } - - throw new Error(localize.DataScience.jupyterNbConvertNotSupported()); - } - - public dispose = () => { - this.isDisposed = true; - } - - private addDirectoryChange = (pythonOutput: string, directoryChange: string): string => { - const newCode = CodeSnippits.ChangeDirectory.join(os.EOL).format(localize.DataScience.importChangeDirectoryComment(), directoryChange); - return newCode.concat(pythonOutput); - } - - // When importing a file, calculate if we can create a %cd so that the relative paths work - private calculateDirectoryChange = (notebookFile: string): string | undefined => { - let directoryChange: string | undefined; - 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 = 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)) { - return directoryChange; - } else { - return undefined; - } - } - - private async createTemplateFile(): Promise<string> { - // Create a temp file on disk - const file = await this.fileSystem.createTemporaryFile('.tpl'); - - // Write our template into it - await fs.appendFile(file.filePath, this.nbconvertTemplate); - - // Save this file into our disposables so the temp file goes away - this.disposableRegistry.push(file); - - // Now we should have a template that will convert - return file.filePath; - } -} diff --git a/src/client/datascience/jupyter/jupyterInstallError.ts b/src/client/datascience/jupyter/jupyterInstallError.ts deleted file mode 100644 index c603b4dd5627..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/jupyterKernelSpec.ts b/src/client/datascience/jupyter/jupyterKernelSpec.ts deleted file mode 100644 index 9881db8671c7..000000000000 --- a/src/client/datascience/jupyter/jupyterKernelSpec.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { Kernel } from '@jupyterlab/services'; -import * as fs from 'fs-extra'; -import * as path from 'path'; - -import { noop } from '../../common/utils/misc'; -import { IJupyterKernelSpec } from '../types'; - -const IsGuidRegEx = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; - -export class JupyterKernelSpec implements IJupyterKernelSpec { - public name: string; - public language: string; - public path: string; - public specFile: string | undefined; - constructor(specModel : Kernel.ISpecModel, file?: string) { - this.name = specModel.name; - this.language = specModel.language; - this.path = specModel.argv && specModel.argv.length > 0 ? specModel.argv[0] : ''; - this.specFile = file; - } - public dispose = async () => { - if (this.specFile && - IsGuidRegEx.test(path.basename(path.dirname(this.specFile)))) { - // There is more than one location for the spec file directory - // to be cleaned up. If one fails, the other likely deleted it already. - try { - await fs.remove(path.dirname(this.specFile)); - } catch { - noop(); - } - this.specFile = undefined; - } - } -} diff --git a/src/client/datascience/jupyter/jupyterServer.ts b/src/client/datascience/jupyter/jupyterServer.ts deleted file mode 100644 index 9435784b5e21..000000000000 --- a/src/client/datascience/jupyter/jupyterServer.ts +++ /dev/null @@ -1,651 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { nbformat } from '@jupyterlab/coreutils'; -import { Kernel, KernelMessage } from '@jupyterlab/services'; -import * as fs from 'fs-extra'; -import { inject, injectable } from 'inversify'; -import * as os from 'os'; -import { Observable } from 'rxjs/Observable'; -import { Subscriber } from 'rxjs/Subscriber'; -import * as vscode from 'vscode'; -import { CancellationToken } from 'vscode-jsonrpc'; - -import { IWorkspaceService } from '../../common/application/types'; -import { CancellationError } from '../../common/cancellation'; -import { IAsyncDisposableRegistry, IDisposable, IDisposableRegistry, ILogger } 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 { generateCells } from '../cellFactory'; -import { concatMultilineString } from '../common'; -import { - CellState, - ICell, - IConnection, - IJupyterKernelSpec, - IJupyterSession, - IJupyterSessionManager, - INotebookServer, - InterruptResult -} from '../types'; - -class CellSubscriber { - private deferred: Deferred<CellState> = createDeferred<CellState>(); - private cellRef: ICell; - private subscriber: Subscriber<ICell>; - private promiseComplete: (self: CellSubscriber) => void; - private startTime: number; - - constructor(cell: ICell, subscriber: Subscriber<ICell>, 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); - } - - // Then see if we're finished or not. - this.attemptToFinish(); - } - - // 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)) { - this.subscriber.next(this.cellRef); - } - this.subscriber.complete(); - - // Then see if we're finished or not. - this.attemptToFinish(); - } - - public reject() { - if (!this.deferred.completed) { - this.cellRef.state = CellState.error; - this.subscriber.next(this.cellRef); - this.subscriber.complete(); - this.deferred.reject(); - this.promiseComplete(this); - } - } - - public get promise(): Promise<CellState> { - return this.deferred.promise; - } - - public get cell(): ICell { - return this.cellRef; - } - - 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 - -@injectable() -export class JupyterServer implements INotebookServer, IDisposable { - private session: IJupyterSession | undefined; - private connInfo: IConnection | undefined; - private workingDir: string | undefined; - private sessionStartTime: number | undefined; - private onStatusChangedEvent: vscode.EventEmitter<boolean> = new vscode.EventEmitter<boolean>(); - private pendingCellSubscriptions: CellSubscriber[] = []; - private ranInitialSetup = false; - - constructor( - @inject(ILogger) private logger: ILogger, - @inject(IWorkspaceService) private workspaceService: IWorkspaceService, - @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, - @inject(IAsyncDisposableRegistry) private asyncRegistry: IAsyncDisposableRegistry, - @inject(IJupyterSessionManager) private sessionManager: IJupyterSessionManager) { - this.asyncRegistry.push(this); - } - - public connect = async (connInfo: IConnection, kernelSpec: IJupyterKernelSpec, cancelToken?: CancellationToken, workingDir?: string): Promise<void> => { - // Save connection info. Determines if we need to change directory or not - this.connInfo = connInfo; - this.workingDir = workingDir; - - // Start our session - this.session = await this.sessionManager.startNew(connInfo, kernelSpec, cancelToken); - - // Setup our start time. We reject anything that comes in before this time during execute - this.sessionStartTime = Date.now(); - - // Wait for it to be ready - await this.session.waitForIdle(); - - // Run our initial setup and plot magics - this.initialNotebookSetup(cancelToken); - } - - public shutdown(): Promise<void> { - const dispose = this.session ? this.session.dispose() : undefined; - return dispose ? dispose : Promise.resolve(); - } - - public dispose(): Promise<void> { - this.onStatusChangedEvent.dispose(); - return this.shutdown(); - } - - public waitForIdle(): Promise<void> { - return this.session ? this.session.waitForIdle() : Promise.resolve(); - } - - public execute(code: string, file: string, line: number, cancelToken?: CancellationToken): Promise<ICell[]> { - // Do initial setup if necessary - this.initialNotebookSetup(); - - // Create a deferred that we'll fire when we're done - const deferred = createDeferred<ICell[]>(); - - // Attempt to evaluate this cell in the jupyter notebook - const observable = this.executeObservable(code, file, line); - 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 setInitialDirectory = async (directory: string): Promise<void> => { - // If we launched local and have no working directory call this on add code to change directory - if (!this.workingDir && this.connInfo && this.connInfo.localLaunch) { - await this.changeDirectoryIfPossible(directory); - this.workingDir = directory; - } - } - - public executeObservable = (code: string, file: string, line: number): Observable<ICell[]> => { - // Do initial setup if necessary - this.initialNotebookSetup(); - - // If we have a session, execute the code now. - if (this.session) { - // Generate our cells ahead of time - const cells = generateCells(code, file, line); - - // 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])); - } else if (cells.length > 0) { - // Either markdown or or code - return this.combineObservables( - cells[0].data.cell_type === 'code' ? this.executeCodeObservable(cells[0]) : this.executeMarkdownObservable(cells[0])); - } - } - - // Can't run because no session - return new Observable<ICell[]>(subscriber => { - subscriber.error(new Error(localize.DataScience.sessionDisposed())); - subscriber.complete(); - }); - } - - public executeSilently = (code: string, cancelToken?: CancellationToken): Promise<void> => { - return new Promise((resolve, reject) => { - - // If we cancel, reject our promise - if (cancelToken) { - this.disposableRegistry.push(cancelToken.onCancellationRequested(() => reject(new CancellationError()))); - } - - // Do initial setup if necessary - this.initialNotebookSetup(); - - // If we have a session, execute the code now. - if (this.session) { - // Generate a new request and resolve when it's done. - const request = this.generateRequest(code, true); - - if (request) { - // // For debugging purposes when silently is failing. - // request.onIOPub = (msg: KernelMessage.IIOPubMessage) => { - // try { - // this.logger.logInformation(`Execute silently message ${msg.header.msg_type} : hasData=${'data' in msg.content}`); - // } catch (err) { - // this.logger.logError(err); - // } - // }; - - request.done.then(() => { - this.logger.logInformation(`Execute for ${code} silently finished.`); - resolve(); - }).catch(reject); - } else { - reject(new Error(localize.DataScience.sessionDisposed())); - } - } else { - reject(new Error(localize.DataScience.sessionDisposed())); - } - }); - } - - public get onStatusChanged(): vscode.Event<boolean> { - return this.onStatusChangedEvent.event.bind(this.onStatusChangedEvent); - } - - public restartKernel = async (): Promise<void> => { - if (this.session) { - // Update our start time so we don't keep sending responses - this.sessionStartTime = Date.now(); - - // Complete all pending as an error. We're restarting - const copyPending = [...this.pendingCellSubscriptions]; - copyPending.forEach(c => c.reject()); - - // Restart our kernel - await this.session.restart(); - - // Rerun our initial setup for the notebook - this.ranInitialSetup = false; - this.initialNotebookSetup(); - - return; - } - - throw new Error(localize.DataScience.sessionDisposed()); - } - - public interruptKernel = async (timeoutMs: number): Promise<InterruptResult> => { - if (this.session) { - // Keep track of our current time. If our start time gets reset, we - // restarted the kernel. - const interruptBeginTime = Date.now(); - - // Copy the list of pending cells. If these don't finish before the timeout - // then our interrupt didn't work. - const copyPending = [...this.pendingCellSubscriptions]; - - // Create a promise that resolves when all of our currently - // pending cells finish. - const finished = copyPending.length > 0 ? - Promise.all(copyPending.map(d => d.promise)) : Promise.resolve([CellState.finished]); - - // Create a deferred promise that resolves if we have a failure - const restarted = createDeferred<CellState[]>(); - - // Listen to status change events so we can tell if we're restarting - const restartHandler = () => { - // We restarted the kernel. - this.sessionStartTime = Date.now(); - this.logger.logWarning('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. - const newCopyPending = [...this.pendingCellSubscriptions]; - newCopyPending.forEach(c => { - c.reject(); - }); - }; - const restartHandlerToken = this.session.onRestarted(restartHandler); - - // Start our interrupt. If it fails, indicate a restart - this.session.interrupt().catch(exc => { - this.logger.logWarning(`Error during interrupt: ${exc}`); - restarted.resolve([]); - }); - - try { - // Wait for all of the pending cells to finish or the timeout to fire - const result = await Promise.race([finished, restarted.promise, sleep(timeoutMs)]); - const states = result as CellState[]; - - // See if we restarted or not - if (restarted.completed) { - return InterruptResult.Restarted; - } - - if (states) { - // We got back the pending cells - return InterruptResult.Success; - } - - // 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; - } 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 new Error(localize.DataScience.sessionDisposed()); - } - - // Return a copy of the connection information that this server used to connect with - public getConnectionInfo(): IConnection | undefined { - if (!this.connInfo) { - return undefined; - } - - // Return a copy with a no-op for dispose - return { - ...this.connInfo, - dispose: noop - }; - } - - private generateRequest = (code: string, silent: boolean): Kernel.IFuture | undefined => { - //this.logger.logInformation(`Executing code in jupyter : ${code}`) - try { - return this.session ? this.session.requestExecute( - { - // Replace windows line endings with unix line endings. - code: code.replace(/\r\n/g, '\n'), - stop_on_error: false, - allow_stdin: false, - silent: silent - }, - true - ) : undefined; - } catch (exc) { - // Any errors generating a request should just be logged. User can't do anything about it. - this.logger.logError(exc); - } - - return undefined; - } - - // Set up our initial plotting and imports - private initialNotebookSetup = (cancelToken?: CancellationToken) => { - if (this.ranInitialSetup) { - return; - } - this.ranInitialSetup = true; - - // When we start our notebook initial, change to our workspace or user specified root directory - if (this.connInfo && this.connInfo.localLaunch && this.workingDir) { - this.changeDirectoryIfPossible(this.workingDir).ignoreErrors(); - } - - // Check for dark theme, if so set matplot lib to use dark_background settings - let darkTheme: boolean = false; - const workbench = this.workspaceService.getConfiguration('workbench'); - if (workbench) { - const theme = workbench.get<string>('colorTheme'); - if (theme) { - darkTheme = /dark/i.test(theme); - } - } - - this.executeSilently( - `%matplotlib inline${os.EOL}import matplotlib.pyplot as plt${darkTheme ? `${os.EOL}from matplotlib import style${os.EOL}style.use(\'dark_background\')` : ''}`, - cancelToken - ).ignoreErrors(); - } - - private combineObservables = (...args: Observable<ICell>[]): Observable<ICell[]> => { - return new Observable<ICell[]>(subscriber => { - // When all complete, we have our results - const results: { [id: string]: ICell } = {}; - - 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<ICell> => { - // Markdown doesn't need any execution - return new Observable<ICell>(subscriber => { - subscriber.next(cell); - subscriber.complete(); - }); - } - - private changeDirectoryIfPossible = async (directory: string): Promise<void> => { - if (this.connInfo && this.connInfo.localLaunch && await fs.pathExists(directory)) { - await this.executeSilently(`%cd "${directory}"`); - } - } - - private handleCodeRequest = (subscriber: CellSubscriber) => { - // Generate a new request if we still can - if (subscriber.isValid(this.sessionStartTime)) { - - const request = this.generateRequest(concatMultilineString(subscriber.cell.data.source), false); - - // tslint:disable-next-line:no-require-imports - const jupyterLab = require('@jupyterlab/services') as typeof import('@jupyterlab/services'); - - // Transition to the busy stage - subscriber.cell.state = CellState.executing; - - // Listen to the reponse messages and update state as we go - if (request) { - request.onIOPub = (msg: KernelMessage.IIOPubMessage) => { - try { - if (jupyterLab.KernelMessage.isExecuteResultMsg(msg)) { - this.handleExecuteResult(msg as KernelMessage.IExecuteResultMsg, subscriber.cell); - } else if (jupyterLab.KernelMessage.isExecuteInputMsg(msg)) { - this.handleExecuteInput(msg as KernelMessage.IExecuteInputMsg, subscriber.cell); - } else if (jupyterLab.KernelMessage.isStatusMsg(msg)) { - this.handleStatusMessage(msg as KernelMessage.IStatusMsg, subscriber.cell); - } else if (jupyterLab.KernelMessage.isStreamMsg(msg)) { - this.handleStreamMesssage(msg as KernelMessage.IStreamMsg, subscriber.cell); - } else if (jupyterLab.KernelMessage.isDisplayDataMsg(msg)) { - this.handleDisplayData(msg as KernelMessage.IDisplayDataMsg, subscriber.cell); - } else if (jupyterLab.KernelMessage.isUpdateDisplayDataMsg(msg)) { - this.handleUpdateDisplayData(msg as KernelMessage.IUpdateDisplayDataMsg, subscriber.cell); - } else if (jupyterLab.KernelMessage.isErrorMsg(msg)) { - this.handleError(msg as KernelMessage.IErrorMsg, subscriber.cell); - } else { - this.logger.logWarning(`Unknown message ${msg.header.msg_type} : hasData=${'data' in msg.content}`); - } - - // Set execution count, all messages should have it - if (msg.content.execution_count) { - subscriber.cell.data.execution_count = msg.content.execution_count as number; - } - - // Show our update if any new output - subscriber.next(this.sessionStartTime); - } catch (err) { - // If not a restart error, then tell the subscriber - subscriber.error(this.sessionStartTime, err); - } - }; - - // When the request finishes we are done - request.done.then(() => subscriber.complete(this.sessionStartTime)).catch(e => subscriber.error(this.sessionStartTime, e)); - } else { - subscriber.error(this.sessionStartTime, new Error(localize.DataScience.sessionDisposed())); - } - } else { - // Otherwise just set to an error - this.handleInterrupted(subscriber.cell); - subscriber.cell.state = CellState.error; - subscriber.complete(this.sessionStartTime); - } - - } - - private executeCodeObservable(cell: ICell): Observable<ICell> { - return new Observable<ICell>(subscriber => { - // Tell our listener. NOTE: have to do this asap so that markdown cells don't get - // run before our cells. - subscriber.next(cell); - - // Wrap the subscriber and save it. It is now pending and waiting completion. - const cellSubscriber = new CellSubscriber(cell, subscriber, (self: CellSubscriber) => { - this.pendingCellSubscriptions = this.pendingCellSubscriptions.filter(p => p !== self); - }); - this.pendingCellSubscriptions.push(cellSubscriber); - - // Attempt to change to the current directory. When that finishes - // send our real request - this.handleCodeRequest(cellSubscriber); - }); - } - - private addToCellData = (cell: ICell, output: nbformat.IUnrecognizedOutput | nbformat.IExecuteResult | nbformat.IDisplayData | nbformat.IStream | nbformat.IError) => { - const data: nbformat.ICodeCell = cell.data as nbformat.ICodeCell; - data.outputs = [...data.outputs, output]; - cell.data = data; - } - - private handleExecuteResult(msg: KernelMessage.IExecuteResultMsg, cell: ICell) { - this.addToCellData(cell, { output_type: 'execute_result', data: msg.content.data, metadata: msg.content.metadata, execution_count: msg.content.execution_count }); - } - - private handleExecuteInput(msg: KernelMessage.IExecuteInputMsg, cell: ICell) { - cell.data.execution_count = msg.content.execution_count; - } - - private handleStatusMessage(msg: KernelMessage.IStatusMsg, cell: ICell) { - if (msg.content.execution_state === 'busy') { - this.onStatusChangedEvent.fire(true); - } else { - this.onStatusChangedEvent.fire(false); - } - - // Status change to idle generally means we finished. Not sure how to - // make sure of this. Maybe only bother if an interrupt - if (msg.content.execution_state === 'idle' && cell.state !== CellState.error) { - cell.state = CellState.finished; - } - } - - private handleStreamMesssage(msg: KernelMessage.IStreamMsg, cell: ICell) { - // Might already have a stream message. If so, just add on to it. - const data: nbformat.ICodeCell = cell.data as nbformat.ICodeCell; - const existing = data.outputs.find(o => o.output_type === 'stream'); - if (existing && existing.name === msg.content.name) { - // tslint:disable-next-line:restrict-plus-operands - existing.text = existing.text + msg.content.text; - } else { - // Create a new stream entry - const output: nbformat.IStream = { - output_type: 'stream', - name: msg.content.name, - text: msg.content.text - }; - this.addToCellData(cell, output); - } - } - - private handleDisplayData(msg: KernelMessage.IDisplayDataMsg, cell: ICell) { - const output: nbformat.IDisplayData = { - output_type: 'display_data', - data: msg.content.data, - metadata: msg.content.metadata - }; - this.addToCellData(cell, output); - } - - private handleUpdateDisplayData(msg: KernelMessage.IUpdateDisplayDataMsg, cell: ICell) { - // Should already have a display data output in our cell. - const data: nbformat.ICodeCell = cell.data as nbformat.ICodeCell; - const output = data.outputs.find(o => o.output_type === 'display_data'); - if (output) { - output.data = msg.content.data; - output.metadata = msg.content.metadata; - } - } - - private handleInterrupted(cell: ICell) { - this.handleError({ - channel: 'iopub', - parent_header: {}, - metadata: {}, - header: { username: '', version: '', session: '', msg_id: '', msg_type: 'error' }, - content: { - ename: 'KeyboardInterrupt', - evalue: '', - // Does this need to be translated? All depends upon if jupyter does or not - traceback: [ - '---------------------------------------------------------------------------', - 'KeyboardInterrupt: ' - ] - } - }, cell); - } - - private handleError(msg: KernelMessage.IErrorMsg, 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); - cell.state = CellState.error; - } -} diff --git a/src/client/datascience/jupyter/jupyterSession.ts b/src/client/datascience/jupyter/jupyterSession.ts deleted file mode 100644 index e0e3daf92a5e..000000000000 --- a/src/client/datascience/jupyter/jupyterSession.ts +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { - Contents, - ContentsManager, - Kernel, - KernelMessage, - ServerConnection, - Session, - SessionManager -} from '@jupyterlab/services'; -import { JSONObject } from '@phosphor/coreutils'; -import { Slot } from '@phosphor/signaling'; -import { Event, EventEmitter } from 'vscode'; -import { CancellationToken } from 'vscode-jsonrpc'; - -import { Cancellation } from '../../common/cancellation'; -import { sleep } from '../../common/utils/async'; -import * as localize from '../../common/utils/localize'; -import { noop } from '../../common/utils/misc'; -import { IConnection, IJupyterKernelSpec, IJupyterSession } from '../types'; - -export class JupyterSession implements IJupyterSession { - private connInfo: IConnection | undefined; - private kernelSpec: IJupyterKernelSpec | undefined; - private sessionManager : SessionManager | undefined; - private session: Session.ISession | undefined; - private contentsManager: ContentsManager | undefined; - private notebookFile: Contents.IModel | undefined; - private onRestartedEvent : EventEmitter<void> = new EventEmitter<void>(); - private statusHandler : Slot<Session.ISession, Kernel.Status> | undefined; - private connected: boolean = false; - - constructor( - connInfo: IConnection, - kernelSpec: IJupyterKernelSpec | undefined) { - this.connInfo = connInfo; - this.kernelSpec = kernelSpec; - } - - public dispose() : Promise<void> { - return this.shutdown(); - } - - public shutdown = async () : Promise<void> => { - await this.destroyKernelSpec(); - - // Destroy the notebook file if not local. Local is cleaned up when we destroy the kernel spec. - if (this.notebookFile && this.contentsManager && this.connInfo && !this.connInfo.localLaunch) { - try { - // Make sure we have a session first and it returns something - if (this.sessionManager) - { - await this.sessionManager.refreshRunning(); - await this.contentsManager.delete(this.notebookFile.path); - } - } catch { - noop(); - } - } - await this.shutdownSessionAndConnection(); - } - - public get onRestarted() : Event<void> { - return this.onRestartedEvent.event.bind(this.onRestartedEvent); - } - - public async waitForIdle() : Promise<void> { - if (this.session && this.session.kernel) { - await this.session.kernel.ready; - - while (this.session.kernel.status !== 'idle') { - await sleep(0); - } - } - } - - public restart() : Promise<void> { - return this.session && this.session.kernel ? this.session.kernel.restart() : Promise.resolve(); - } - - public interrupt() : Promise<void> { - return this.session && this.session.kernel ? this.session.kernel.interrupt() : Promise.resolve(); - } - - public requestExecute(content: KernelMessage.IExecuteRequest, disposeOnDone?: boolean, metadata?: JSONObject) : Kernel.IFuture | undefined { - return this.session && this.session.kernel ? this.session.kernel.requestExecute(content, disposeOnDone, metadata) : undefined; - } - - public async connect(cancelToken?: CancellationToken) : Promise<void> { - if (!this.connInfo) { - throw new Error(localize.DataScience.sessionDisposed()); - } - - // First connect to the sesssion manager - const serverSettings = ServerConnection.makeSettings( - { - baseUrl: this.connInfo.baseUrl, - token: this.connInfo.token, - pageUrl: '', - // A web socket is required to allow token authentication - wsUrl: this.connInfo.baseUrl.replace('http', 'ws'), - init: { cache: 'no-store', credentials: 'same-origin' } - }); - this.sessionManager = new SessionManager({ serverSettings: serverSettings }); - - // Create a temporary .ipynb file to use - this.contentsManager = new ContentsManager({ serverSettings: serverSettings }); - this.notebookFile = await this.contentsManager.newUntitled({type: 'notebook'}); - - // Create our session options using this temporary notebook and our connection info - const options: Session.IOptions = { - path: this.notebookFile.path, - kernelName: this.kernelSpec ? this.kernelSpec.name : '', - serverSettings: serverSettings - }; - - // Start a new session - this.session = await Cancellation.race(() => this.sessionManager!.startNew(options), cancelToken); - - // Listen for session status changes - this.statusHandler = this.onStatusChanged.bind(this.onStatusChanged); - this.session.statusChanged.connect(this.statusHandler); - - // Made it this far, we're connected now - this.connected = true; - } - - public get isConnected() : boolean { - return this.connected; - } - - private onStatusChanged(s: Session.ISession, a: Kernel.Status) { - if (a === 'starting') { - this.onRestartedEvent.fire(); - } - } - - private async destroyKernelSpec() { - try { - if (this.kernelSpec) { - await this.kernelSpec.dispose(); // This should delete any old kernel specs - } - } catch { - noop(); - } - this.kernelSpec = undefined; - } - - private shutdownSessionAndConnection = async () => { - if (this.contentsManager) { - this.contentsManager.dispose(); - this.contentsManager = undefined; - } - if (this.session || this.sessionManager) { - try { - if (this.statusHandler && this.session) { - this.session.statusChanged.disconnect(this.statusHandler); - this.statusHandler = undefined; - } - if (this.session) { - // Shutdown may fail if the process has been killed - await Promise.race([this.session.shutdown(), sleep(100)]); - this.session.dispose(); - } - if (this.sessionManager) { - this.sessionManager.dispose(); - } - } catch { - if (this.session) { - this.session.dispose(); - } - if (this.sessionManager) { - this.sessionManager.dispose(); - } - } - this.session = undefined; - this.sessionManager = undefined; - } - this.onRestartedEvent.dispose(); - if (this.connInfo) { - this.connInfo.dispose(); // This should kill the process that's running - this.connInfo = undefined; - } - } - -} diff --git a/src/client/datascience/jupyter/jupyterSessionManager.ts b/src/client/datascience/jupyter/jupyterSessionManager.ts deleted file mode 100644 index a8a8823cf73c..000000000000 --- a/src/client/datascience/jupyter/jupyterSessionManager.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { ServerConnection, SessionManager } from '@jupyterlab/services'; -import { injectable } from 'inversify'; -import { CancellationToken } from 'vscode-jsonrpc'; - -import { IConnection, IJupyterKernelSpec, IJupyterSession, IJupyterSessionManager } from '../types'; -import { JupyterKernelSpec } from './jupyterKernelSpec'; -import { JupyterSession } from './jupyterSession'; - -@injectable() -export class JupyterSessionManager implements IJupyterSessionManager { - - public async startNew(connInfo: IConnection, kernelSpec: IJupyterKernelSpec, cancelToken?: CancellationToken) : Promise<IJupyterSession> { - // Create a new session and attempt to connect to it - const session = new JupyterSession(connInfo, kernelSpec); - try { - await session.connect(cancelToken); - } finally { - if (!session.isConnected) { - await session.dispose(); - } - } - return session; - } - - public async getActiveKernelSpecs(connection: IConnection) : Promise<IJupyterKernelSpec[]> { - // Use our connection to create a session manager - const serverSettings = ServerConnection.makeSettings( - { - baseUrl: connection.baseUrl, - token: connection.token, - pageUrl: '', - // A web socket is required to allow token authentication (what if there is no token authentication?) - wsUrl: connection.baseUrl.replace('http', 'ws'), - init: { cache: 'no-store', credentials: 'same-origin' } - }); - const sessionManager = new SessionManager({ serverSettings: serverSettings }); - try { - // Ask the session manager to refresh its list of kernel specs. - await sessionManager.refreshSpecs(); - - // Enumerate all of the kernel specs, turning each into a JupyterKernelSpec - const kernelspecs = sessionManager.specs && sessionManager.specs.kernelspecs ? sessionManager.specs.kernelspecs : {}; - const keys = Object.keys(kernelspecs); - return keys.map(k => { - const spec = kernelspecs[k]; - return new JupyterKernelSpec(spec); - }); - } catch { - // For some reason this is failing. Just return nothing - return []; - } finally { - // Cleanup the session manager as we don't need it anymore - if (sessionManager) { - sessionManager.dispose(); - } - } - - } - -} diff --git a/src/client/datascience/serviceRegistry.ts b/src/client/datascience/serviceRegistry.ts deleted file mode 100644 index 9c63e765e9a8..000000000000 --- a/src/client/datascience/serviceRegistry.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { IServiceManager } from '../ioc/types'; -import { CodeCssGenerator } from './codeCssGenerator'; -import { DataScience } from './datascience'; -import { DataScienceCodeLensProvider } from './editor-integration/codelensprovider'; -import { CodeWatcher } from './editor-integration/codewatcher'; -import { History } from './history'; -import { HistoryCommandListener } from './historycommandlistener'; -import { HistoryProvider } from './historyProvider'; -import { JupyterExecution } from './jupyter/jupyterExecution'; -import { JupyterExporter } from './jupyter/jupyterExporter'; -import { JupyterImporter } from './jupyter/jupyterImporter'; -import { JupyterServer } from './jupyter/jupyterServer'; -import { JupyterSessionManager } from './jupyter/jupyterSessionManager'; -import { StatusProvider } from './statusProvider'; -import { - ICodeCssGenerator, - ICodeWatcher, - IDataScience, - IDataScienceCodeLensProvider, - IDataScienceCommandListener, - IHistory, - IHistoryProvider, - IJupyterExecution, - IJupyterSessionManager, - INotebookExporter, - INotebookImporter, - INotebookServer, - IStatusProvider -} from './types'; - -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton<IDataScienceCodeLensProvider>(IDataScienceCodeLensProvider, DataScienceCodeLensProvider); - serviceManager.addSingleton<IDataScience>(IDataScience, DataScience); - serviceManager.addSingleton<IJupyterExecution>(IJupyterExecution, JupyterExecution); - serviceManager.add<IDataScienceCommandListener>(IDataScienceCommandListener, HistoryCommandListener); - serviceManager.addSingleton<IHistoryProvider>(IHistoryProvider, HistoryProvider); - serviceManager.add<IHistory>(IHistory, History); - serviceManager.add<INotebookExporter>(INotebookExporter, JupyterExporter); - serviceManager.add<INotebookImporter>(INotebookImporter, JupyterImporter); - serviceManager.add<INotebookServer>(INotebookServer, JupyterServer); - serviceManager.addSingleton<ICodeCssGenerator>(ICodeCssGenerator, CodeCssGenerator); - serviceManager.addSingleton<IStatusProvider>(IStatusProvider, StatusProvider); - serviceManager.addSingleton<IJupyterSessionManager>(IJupyterSessionManager, JupyterSessionManager); - serviceManager.add<ICodeWatcher>(ICodeWatcher, CodeWatcher); -} diff --git a/src/client/datascience/statusProvider.ts b/src/client/datascience/statusProvider.ts deleted file mode 100644 index 06a8c121b119..000000000000 --- a/src/client/datascience/statusProvider.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 { Disposable, ProgressLocation, ProgressOptions } from 'vscode'; - -import { IApplicationShell } from '../common/application/types'; -import { createDeferred, Deferred } from '../common/utils/async'; -import { HistoryMessages } from './constants'; -import { IHistoryProvider, IStatusProvider } from './types'; - -class StatusItem implements Disposable { - - private deferred : Deferred<void>; - private disposed: boolean = false; - private timeout: NodeJS.Timer | undefined; - private disposeCallback: () => void; - - constructor(title: string, disposeCallback: () => void, timeout?: number) { - this.deferred = createDeferred<void>(); - 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) { - clearTimeout(this.timeout); - this.timeout = undefined; - } - this.disposeCallback(); - if (!this.deferred.completed) { - this.deferred.resolve(); - } - } - } - - public promise = () : Promise<void> => { - 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, - @inject(IHistoryProvider) private historyProvider: IHistoryProvider) { - } - - public set(message: string, timeout?: number, cancel?: () => void) : Disposable { - // Start our progress - this.incrementCount(); - - // Create a StatusItem that will return our promise - const statusItem = new StatusItem(message, () => this.decrementCount(), 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<T>(promise: () => Promise<T>, message: string, timeout?: number, cancel?: () => void) : Promise<T> { - // Create a status item and wait for our promise to either finish or reject - const status = this.set(message, timeout, cancel); - let result : T; - try { - result = await promise(); - } finally { - status.dispose(); - } - return result; - } - - private incrementCount = () => { - if (this.statusCount === 0) { - const history = this.historyProvider.getActive(); - if (history) { - history.postMessage(HistoryMessages.StartProgress); - } - } - this.statusCount += 1; - } - - private decrementCount = () => { - const updatedCount = this.statusCount - 1; - if (updatedCount === 0) { - const history = this.historyProvider.getActive(); - if (history) { - history.postMessage(HistoryMessages.StopProgress); - } - } - this.statusCount = Math.max(updatedCount, 0); - } - -} diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts deleted file mode 100644 index ba60905249f2..000000000000 --- a/src/client/datascience/types.ts +++ /dev/null @@ -1,190 +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/lib/kernel'; -import { JSONObject } from '@phosphor/coreutils'; -import { Observable } from 'rxjs/Observable'; -import { CancellationToken, CodeLens, CodeLensProvider, Disposable, Event, Range, TextDocument, TextEditor } from 'vscode'; - -import { ICommandManager } from '../common/application/types'; -import { IDisposable } from '../common/types'; -import { PythonInterpreter } from '../interpreter/contracts'; - -// Main interface -export const IDataScience = Symbol('IDataScience'); -export interface IDataScience extends Disposable { - activate(): Promise<void>; -} - -export const IDataScienceCommandListener = Symbol('IDataScienceCommandListener'); -export interface IDataScienceCommandListener { - register(commandManager: ICommandManager); -} - -// Connection information for talking to a jupyter notebook process -export interface IConnection extends Disposable { - baseUrl: string; - token: string; - localLaunch: boolean; -} - -export enum InterruptResult { - Success = 0, - TimedOut = 1, - Restarted = 2 -} - -// Talks to a jupyter ipython kernel to retrieve data for cells -export const INotebookServer = Symbol('INotebookServer'); -export interface INotebookServer extends Disposable { - onStatusChanged: Event<boolean>; - connect(conninfo: IConnection, kernelSpec: IJupyterKernelSpec, cancelToken?: CancellationToken, workingDir?: string) : Promise<void>; - executeObservable(code: string, file: string, line: number) : Observable<ICell[]>; - execute(code: string, file: string, line: number, cancelToken?: CancellationToken) : Promise<ICell[]>; - restartKernel() : Promise<void>; - waitForIdle() : Promise<void>; - shutdown() : Promise<void>; - interruptKernel(timeoutInMs: number) : Promise<InterruptResult>; - setInitialDirectory(directory: string): Promise<void>; - getConnectionInfo(): IConnection | undefined; -} - -export const IJupyterExecution = Symbol('IJupyterExecution'); -export interface IJupyterExecution { - isNotebookSupported(cancelToken?: CancellationToken) : Promise<boolean>; - isImportSupported(cancelToken?: CancellationToken) : Promise<boolean>; - isKernelCreateSupported(cancelToken?: CancellationToken): Promise<boolean>; - connectToNotebookServer(uri: string | undefined, useDefaultConfig: boolean, cancelToken?: CancellationToken, workingDir?: string) : Promise<INotebookServer | undefined>; - spawnNotebook(file: string) : Promise<void>; - importNotebook(file: string, template: string) : Promise<string>; - getUsableJupyterPython(cancelToken?: CancellationToken) : Promise<PythonInterpreter | undefined>; -} - -export const IJupyterSession = Symbol('IJupyterSession'); -export interface IJupyterSession extends IDisposable { - onRestarted: Event<void>; - restart() : Promise<void>; - interrupt() : Promise<void>; - waitForIdle() : Promise<void>; - requestExecute(content: KernelMessage.IExecuteRequest, disposeOnDone?: boolean, metadata?: JSONObject) : Kernel.IFuture | undefined; -} -export const IJupyterSessionManager = Symbol('IJupyterSessionManager'); -export interface IJupyterSessionManager { - startNew(connInfo: IConnection, kernelSpec: IJupyterKernelSpec, cancelToken?: CancellationToken) : Promise<IJupyterSession>; - getActiveKernelSpecs(connInfo: IConnection) : Promise<IJupyterKernelSpec[]>; -} - -export interface IJupyterKernelSpec extends IDisposable { - name: string | undefined; - language: string | undefined; - path: string | undefined; -} - -export const INotebookImporter = Symbol('INotebookImporter'); -export interface INotebookImporter extends Disposable { - importFromFile(file: string) : Promise<string>; -} - -export const INotebookExporter = Symbol('INotebookExporter'); -export interface INotebookExporter extends Disposable { - translateToNotebook(cells: ICell[], directoryChange?: string) : Promise<JSONObject | undefined>; -} - -export const IHistoryProvider = Symbol('IHistoryProvider'); -export interface IHistoryProvider { - getActive() : IHistory | undefined; - - getOrCreateActive(): IHistory; -} - -export const IHistory = Symbol('IHistory'); -export interface IHistory extends Disposable { - closed: Event<IHistory>; - show() : Promise<void>; - addCode(code: string, file: string, line: number, editor?: TextEditor) : Promise<void>; - // tslint:disable-next-line:no-any - postMessage(type: string, payload?: any); - undoCells(); - redoCells(); - removeAllCells(); - interruptKernel(); - restartKernel(); - expandAllCells(); - collapseAllCells(); - exportCells(); -} - -// 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); - // tslint:disable-next-line:no-any - listen(message: string, listener: (args: any[] | undefined) => 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 { - addFile(document: TextDocument); - getFileName() : string; - getVersion() : number; - getCodeLenses() : CodeLens[]; - runAllCells(); - runCell(range: Range); - runCurrentCell(); - runCurrentCellAndAdvance(); -} - -export enum CellState { - init = 0, - executing = 1, - finished = 2, - error = 3 -} - -// Basic structure for a cell from a notebook -export interface ICell { - id: string; - file: string; - line: number; - state: CellState; - data: nbformat.ICodeCell | nbformat.IRawCell | nbformat.IMarkdownCell | ISysInfo; -} - -export interface IHistoryInfo { - cellCount: number; - undoCount: number; - redoCount: number; -} - -export interface ISysInfo extends nbformat.IBaseCell { - cell_type: 'sys_info'; - version: string; - notebook_version: string; - path: string; - message: string; - connection: string; -} - -export const ICodeCssGenerator = Symbol('ICodeCssGenerator'); -export interface ICodeCssGenerator { - generateThemeCss() : Promise<string>; -} - -export const IStatusProvider = Symbol('IStatusProvider'); -export interface IStatusProvider { - // call this function to set the new status on the active - // history window. Dispose of the returned object when done. - set(message: string, timeout?: number) : Disposable; - - // call this function to wait for a promise while displaying status - waitWithStatus<T>(promise: () => Promise<T>, message: string, timeout?: number, canceled?: () => void) : Promise<T>; -} diff --git a/src/client/debugger/constants.ts b/src/client/debugger/constants.ts index 2ca5f1c3289c..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 PTVSD_PATH = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python'); export const DebuggerTypeName = 'python'; +export const PythonDebuggerTypeName = 'debugpy'; diff --git a/src/client/debugger/debugAdapter/Common/Contracts.ts b/src/client/debugger/debugAdapter/Common/Contracts.ts deleted file mode 100644 index 0bef9ea82d45..000000000000 --- a/src/client/debugger/debugAdapter/Common/Contracts.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -export interface IDebugServer { - port: number; - host?: string; -} diff --git a/src/client/debugger/debugAdapter/Common/debugStreamProvider.ts b/src/client/debugger/debugAdapter/Common/debugStreamProvider.ts deleted file mode 100644 index 4a3d47078fa4..000000000000 --- a/src/client/debugger/debugAdapter/Common/debugStreamProvider.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { createServer, Socket } from 'net'; -import { isTestExecution } from '../../../common/constants'; -import { ICurrentProcess } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { IDebugStreamProvider } from '../types'; - -@injectable() -export class DebugStreamProvider implements IDebugStreamProvider { - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { } - public get useDebugSocketStream(): boolean { - return this.getDebugPort() > 0; - } - public async getInputAndOutputStreams(): Promise<{ input: NodeJS.ReadStream | Socket; output: NodeJS.WriteStream | Socket }> { - const debugPort = this.getDebugPort(); - let debugSocket: Promise<Socket> | undefined; - - if (debugPort > 0) { - // This section is what allows VS Code extension developers to attach to the current debugger. - // Used in scenarios where extension developers would like to debug the debugger. - debugSocket = new Promise<Socket>(resolve => { - // start as a server, and print to console in VS Code debugger for extension developer. - // Do not print this out when running unit tests. - if (!isTestExecution()) { - console.error(`waiting for debug protocol on port ${debugPort}`); - } - createServer((socket) => { - if (!isTestExecution()) { - console.error('>> accepted connection from client'); - } - resolve(socket); - }).listen(debugPort); - }); - } - - const currentProcess = this.serviceContainer.get<ICurrentProcess>(ICurrentProcess); - const input = debugSocket ? await debugSocket : currentProcess.stdin; - const output = debugSocket ? await debugSocket : currentProcess.stdout; - - return { input, output }; - } - private getDebugPort() { - const currentProcess = this.serviceContainer.get<ICurrentProcess>(ICurrentProcess); - - let debugPort = 0; - const args = currentProcess.argv.slice(2); - args.forEach((val, index, array) => { - const portMatch = /^--server=(\d{4,5})$/.exec(val); - if (portMatch) { - debugPort = parseInt(portMatch[1], 10); - } - }); - return debugPort; - } -} diff --git a/src/client/debugger/debugAdapter/Common/processServiceFactory.ts b/src/client/debugger/debugAdapter/Common/processServiceFactory.ts deleted file mode 100644 index 20dd027a2600..000000000000 --- a/src/client/debugger/debugAdapter/Common/processServiceFactory.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { ProcessService } from '../../../common/process/proc'; -import { IBufferDecoder, IProcessService, IProcessServiceFactory } from '../../../common/process/types'; -import { IServiceContainer } from '../../../ioc/types'; - -@injectable() -export class DebuggerProcessServiceFactory implements IProcessServiceFactory { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } - public create(): Promise<IProcessService> { - const processService = new ProcessService(this.serviceContainer.get<IBufferDecoder>(IBufferDecoder), process.env); - return Promise.resolve(processService); - } -} diff --git a/src/client/debugger/debugAdapter/Common/protocolLogger.ts b/src/client/debugger/debugAdapter/Common/protocolLogger.ts deleted file mode 100644 index 5fcdd5cb3f77..000000000000 --- a/src/client/debugger/debugAdapter/Common/protocolLogger.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { injectable } from 'inversify'; -import { Readable } from 'stream'; -import { Logger } from 'vscode-debugadapter'; -import { IProtocolLogger } from '../types'; - -@injectable() -export class ProtocolLogger implements IProtocolLogger { - private inputStream?: Readable; - private outputStream?: Readable; - private messagesToLog: string[] = []; - private logger?: Logger.ILogger; - public dispose() { - if (this.inputStream) { - this.inputStream.removeListener('data', this.fromDataCallbackHandler); - this.outputStream!.removeListener('data', this.toDataCallbackHandler); - this.messagesToLog = []; - this.inputStream = undefined; - this.outputStream = undefined; - } - } - public connect(inputStream: Readable, outputStream: Readable) { - this.inputStream = inputStream; - this.outputStream = outputStream; - - inputStream.addListener('data', this.fromDataCallbackHandler); - outputStream.addListener('data', this.toDataCallbackHandler); - } - public setup(logger: Logger.ILogger) { - this.logger = logger; - this.logMessages([`Started @ ${new Date().toString()}`]); - this.logMessages(this.messagesToLog); - this.messagesToLog = []; - } - private fromDataCallbackHandler = (data: string | Buffer) => { - this.logMessages(['From Client:', (data as Buffer).toString('utf8')]); - } - private toDataCallbackHandler = (data: string | Buffer) => { - this.logMessages(['To Client:', (data as Buffer).toString('utf8')]); - } - private logMessages(messages: string[]) { - if (this.logger) { - messages.forEach(message => this.logger!.verbose(`${message}`)); - } else { - this.messagesToLog.push(...messages); - } - } -} diff --git a/src/client/debugger/debugAdapter/Common/protocolParser.ts b/src/client/debugger/debugAdapter/Common/protocolParser.ts deleted file mode 100644 index 8a97abec0901..000000000000 --- a/src/client/debugger/debugAdapter/Common/protocolParser.ts +++ /dev/null @@ -1,116 +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'; - -/** - * Parsers the debugger Protocol messages and raises the following events: - * 1. 'data', message (for all protocol messages) - * 1. 'event_<event name>', message (for all protocol events) - * 1. 'request_<command name>', message (for all protocol requests) - * 1. 'response_<command name>', message (for all protocol responses) - * 1. '<type>', message (for all protocol messages that are not events, requests nor responses) - * @export - * @class ProtocolParser - * @extends {EventEmitter} - * @implements {IProtocolParser} - */ -@injectable() -export class ProtocolParser extends EventEmitter implements IProtocolParser { - private rawData = new Buffer(0); - private contentLength: number = -1; - private disposed: boolean = false; - private stream?: Readable; - constructor() { - super(); - } - 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); - } - 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.emit(`${message.type}_${event.event}`, event); - break; - } - } - case 'request': { - const request = message as DebugProtocol.Request; - if (typeof request.command === 'string') { - this.emit(`${message.type}_${request.command}`, request); - break; - } - } - case 'response': { - const reponse = message as DebugProtocol.Response; - if (typeof reponse.command === 'string') { - this.emit(`${message.type}_${reponse.command}`, reponse); - break; - } - } - default: { - this.emit(`${message.type}`, message); - } - } - - this.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/debugAdapter/Common/protocolWriter.ts b/src/client/debugger/debugAdapter/Common/protocolWriter.ts deleted file mode 100644 index 305830a34dc4..000000000000 --- a/src/client/debugger/debugAdapter/Common/protocolWriter.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { injectable } from 'inversify'; -import { Socket } from 'net'; -import { Message } from 'vscode-debugadapter/lib/messages'; -import { IProtocolMessageWriter } from '../types'; - -const TWO_CRLF = '\r\n\r\n'; - -@injectable() -export class ProtocolMessageWriter implements IProtocolMessageWriter { - public write(stream: Socket | NodeJS.WriteStream, message: Message): void { - const json = JSON.stringify(message); - const length = Buffer.byteLength(json, 'utf8'); - - stream.write(`Content-Length: ${length.toString()}${TWO_CRLF}`, 'utf8'); - stream.write(json, 'utf8'); - } -} diff --git a/src/client/debugger/debugAdapter/DebugClients/DebugClient.ts b/src/client/debugger/debugAdapter/DebugClients/DebugClient.ts deleted file mode 100644 index 220bff7fde74..000000000000 --- a/src/client/debugger/debugAdapter/DebugClients/DebugClient.ts +++ /dev/null @@ -1,31 +0,0 @@ -// tslint:disable:quotemark ordered-imports no-any no-empty - -import { BaseDebugServer } from "../DebugServers/BaseDebugServer"; -import { IDebugServer } from "../Common/Contracts"; -import { DebugSession } from "vscode-debugadapter"; -import { EventEmitter } from 'events'; -import { IServiceContainer } from "../../../ioc/types"; - -export enum DebugType { - Local, - Remote, - RunLocal -} -export abstract class DebugClient<T> extends EventEmitter { - protected debugSession: DebugSession; - constructor(protected args: T, debugSession: DebugSession) { - super(); - this.debugSession = debugSession; - } - public abstract CreateDebugServer(serviceContainer?: IServiceContainer): BaseDebugServer ; - public get DebugType(): DebugType { - return DebugType.Local; - } - - public Stop() { - } - - public LaunchApplicationToDebug(dbgServer: IDebugServer): Promise<any> { - return Promise.resolve(); - } -} diff --git a/src/client/debugger/debugAdapter/DebugClients/DebugFactory.ts b/src/client/debugger/debugAdapter/DebugClients/DebugFactory.ts deleted file mode 100644 index 8a3f5dc665db..000000000000 --- a/src/client/debugger/debugAdapter/DebugClients/DebugFactory.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { DebugSession } from 'vscode-debugadapter'; -import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; -import { ILocalDebugLauncherScriptProvider } from '../types'; -import { DebugClient } from './DebugClient'; -import { DebuggerLauncherScriptProvider, NoDebugLauncherScriptProvider } from './launcherProvider'; -import { LocalDebugClient } from './LocalDebugClient'; -import { LocalDebugClientV2 } from './localDebugClientV2'; -import { NonDebugClientV2 } from './nonDebugClientV2'; -import { RemoteDebugClient } from './RemoteDebugClient'; - -export function CreateLaunchDebugClient(launchRequestOptions: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean): DebugClient<{}> { - let launchScriptProvider: ILocalDebugLauncherScriptProvider; - let debugClientClass: typeof LocalDebugClient; - if (launchRequestOptions.noDebug === true) { - launchScriptProvider = new NoDebugLauncherScriptProvider(); - debugClientClass = NonDebugClientV2; - } else { - launchScriptProvider = new DebuggerLauncherScriptProvider(); - debugClientClass = LocalDebugClientV2; - } - return new debugClientClass(launchRequestOptions, debugSession, canLaunchTerminal, launchScriptProvider); -} -export function CreateAttachDebugClient(attachRequestOptions: AttachRequestArguments, debugSession: DebugSession): DebugClient<{}> { - return new RemoteDebugClient(attachRequestOptions, debugSession); -} diff --git a/src/client/debugger/debugAdapter/DebugClients/LocalDebugClient.ts b/src/client/debugger/debugAdapter/DebugClients/LocalDebugClient.ts deleted file mode 100644 index 4513ee9bbe39..000000000000 --- a/src/client/debugger/debugAdapter/DebugClients/LocalDebugClient.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { ChildProcess, spawn } from 'child_process'; -import * as path from 'path'; -import { DebugSession, OutputEvent } from 'vscode-debugadapter'; -import { DebugProtocol } from 'vscode-debugprotocol'; -import { open } from '../../../common/open'; -import { IS_WINDOWS } from '../../../common/platform/constants'; -import { PathUtils } from '../../../common/platform/pathUtils'; -import { CurrentProcess } from '../../../common/process/currentProcess'; -import { noop } from '../../../common/utils/misc'; -import { EnvironmentVariablesService } from '../../../common/variables/environment'; -import { IServiceContainer } from '../../../ioc/types'; -import { LaunchRequestArguments } from '../../types'; -import { IDebugServer } from '../Common/Contracts'; -import { BaseDebugServer } from '../DebugServers/BaseDebugServer'; -import { LocalDebugServerV2 } from '../DebugServers/LocalDebugServerV2'; -import { ILocalDebugLauncherScriptProvider } from '../types'; -import { DebugClient, DebugType } from './DebugClient'; -import { DebugClientHelper } from './helper'; - -enum DebugServerStatus { - Unknown = 1, - Running = 2, - NotRunning = 3 -} - -export class LocalDebugClient extends DebugClient<LaunchRequestArguments> { - protected pyProc: ChildProcess | undefined; - protected debugServer: BaseDebugServer | undefined; - private get debugServerStatus(): DebugServerStatus { - if (this.debugServer && this.debugServer!.IsRunning) { - return DebugServerStatus.Running; - } - if (this.debugServer && !this.debugServer!.IsRunning) { - return DebugServerStatus.NotRunning; - } - return DebugServerStatus.Unknown; - } - constructor(args: LaunchRequestArguments, debugSession: DebugSession, private canLaunchTerminal: boolean, protected launcherScriptProvider: ILocalDebugLauncherScriptProvider) { - super(args, debugSession); - } - - public CreateDebugServer(serviceContainer?: IServiceContainer): BaseDebugServer { - this.debugServer = new LocalDebugServerV2(this.debugSession, this.args, serviceContainer!); - return this.debugServer; - } - - public get DebugType(): DebugType { - return DebugType.Local; - } - - public Stop() { - if (this.debugServer) { - this.debugServer!.Stop(); - this.debugServer = undefined; - } - if (this.pyProc) { - this.pyProc.kill(); - this.pyProc = undefined; - } - } - // tslint:disable-next-line:no-any - private displayError(error: any) { - const errorMsg = typeof error === 'string' ? error : ((error.message && error.message.length > 0) ? error.message : ''); - if (errorMsg.length > 0) { - this.debugSession.sendEvent(new OutputEvent(errorMsg, 'stderr')); - } - } - // tslint:disable-next-line:max-func-body-length member-ordering no-any - public async LaunchApplicationToDebug(dbgServer: IDebugServer): Promise<any> { - const pathUtils = new PathUtils(IS_WINDOWS); - const currentProcess = new CurrentProcess(); - const environmentVariablesService = new EnvironmentVariablesService(pathUtils); - const helper = new DebugClientHelper(environmentVariablesService, pathUtils, currentProcess); - const environmentVariables = await helper.getEnvironmentVariables(this.args); - // tslint:disable-next-line:max-func-body-length cyclomatic-complexity no-any - return new Promise<any>((resolve, reject) => { - const fileDir = this.args && this.args.program ? path.dirname(this.args.program) : ''; - let processCwd = fileDir; - if (typeof this.args.cwd === 'string' && this.args.cwd.length > 0 && this.args.cwd !== 'null') { - processCwd = this.args.cwd; - } - let pythonPath = 'python'; - if (typeof this.args.pythonPath === 'string' && this.args.pythonPath.trim().length > 0) { - pythonPath = this.args.pythonPath; - } - const args = this.buildLaunchArguments(processCwd, dbgServer.port); - switch (this.args.console) { - case 'externalTerminal': - case 'integratedTerminal': { - const isSudo = Array.isArray(this.args.debugOptions) && this.args.debugOptions.some(opt => opt === 'Sudo'); - this.launchExternalTerminal(isSudo, processCwd, pythonPath, args, environmentVariables).then(resolve).catch(reject); - break; - } - default: { - this.pyProc = spawn(pythonPath, args, { cwd: processCwd, env: environmentVariables }); - this.handleProcessOutput(this.pyProc!, reject); - - // Here we wait for the application to connect to the socket server. - // Only once connected do we know that the application has successfully launched. - this.debugServer!.DebugClientConnected - .then(resolve) - .catch(ex => console.error('Python Extension: debugServer.DebugClientConnected', ex)); - } - } - }); - } - // tslint:disable-next-line:member-ordering - protected handleProcessOutput(proc: ChildProcess, failedToLaunch: (error: Error | string | Buffer) => void) { - proc.on('error', error => { - // If debug server has started, then don't display errors. - // The debug adapter will get this info from the debugger (e.g. ptvsd lib). - const status = this.debugServerStatus; - if (status === DebugServerStatus.Running) { - return; - } - if (status === DebugServerStatus.NotRunning && typeof (error) === 'object' && error !== null) { - return failedToLaunch(error); - } - // This could happen when the debugger didn't launch at all, e.g. python doesn't exist. - this.displayError(error); - }); - proc.stderr.setEncoding('utf8'); - proc.stderr.on('data', noop); - proc.stdout.on('data', d => { - // This is necessary so we read the stdout of the python process, - // Else it just keep building up (related to issue #203 and #52). - // tslint:disable-next-line:prefer-const no-unused-variable - let x = 0; - }); - } - private buildLaunchArguments(cwd: string, debugPort: number): string[] { - return [...this.buildDebugArguments(cwd, debugPort), ...this.buildStandardArguments()]; - } - - // tslint:disable-next-line:member-ordering - protected buildDebugArguments(cwd: string, debugPort: number): string[] { - throw new Error('Not Implemented'); - } - // tslint:disable-next-line:member-ordering - protected buildStandardArguments() { - const programArgs = Array.isArray(this.args.args) && this.args.args.length > 0 ? this.args.args : []; - if (typeof this.args.module === 'string' && this.args.module.length > 0) { - return ['-m', this.args.module, ...programArgs]; - } - if (this.args.program && this.args.program.length > 0) { - return [this.args.program, ...programArgs]; - } - return programArgs; - } - private launchExternalTerminal(sudo: boolean, cwd: string, pythonPath: string, args: string[], env: {}) { - return new Promise((resolve, reject) => { - if (this.canLaunchTerminal) { - const command = sudo ? 'sudo' : pythonPath; - const commandArgs = sudo ? [pythonPath].concat(args) : args; - const isExternalTerminal = this.args.console === 'externalTerminal'; - const consoleKind = isExternalTerminal ? 'external' : 'integrated'; - const termArgs: DebugProtocol.RunInTerminalRequestArguments = { - kind: consoleKind, - title: 'Python Debug Console', - cwd, - args: [command].concat(commandArgs), - env - }; - this.debugSession.runInTerminalRequest(termArgs, 5000, (response) => { - if (response.success) { - resolve(); - } else { - reject(response); - } - }); - } else { - open({ wait: false, app: [pythonPath].concat(args), cwd, env, sudo: sudo }).then(proc => { - this.pyProc = proc; - resolve(); - }, error => { - if (this.debugServerStatus === DebugServerStatus.Running) { - return; - } - reject(error); - }); - } - }); - } -} diff --git a/src/client/debugger/debugAdapter/DebugClients/RemoteDebugClient.ts b/src/client/debugger/debugAdapter/DebugClients/RemoteDebugClient.ts deleted file mode 100644 index 9259e5028062..000000000000 --- a/src/client/debugger/debugAdapter/DebugClients/RemoteDebugClient.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { DebugSession } from 'vscode-debugadapter'; -import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; -import { BaseDebugServer } from '../DebugServers/BaseDebugServer'; -import { RemoteDebugServerV2 } from '../DebugServers/RemoteDebugServerv2'; -import { DebugClient, DebugType } from './DebugClient'; - -export class RemoteDebugClient<T extends AttachRequestArguments | LaunchRequestArguments> extends DebugClient<T> { - private debugServer?: BaseDebugServer; - // tslint:disable-next-line:no-any - constructor(args: T, debugSession: DebugSession) { - super(args, debugSession); - } - - public CreateDebugServer(): BaseDebugServer { - this.debugServer = new RemoteDebugServerV2(this.debugSession, this.args); - return this.debugServer; - } - public get DebugType(): DebugType { - return DebugType.Remote; - } - - public Stop() { - if (this.debugServer) { - this.debugServer.Stop(); - this.debugServer = undefined; - } - } - -} diff --git a/src/client/debugger/debugAdapter/DebugClients/helper.ts b/src/client/debugger/debugAdapter/DebugClients/helper.ts deleted file mode 100644 index c7b580c41d5a..000000000000 --- a/src/client/debugger/debugAdapter/DebugClients/helper.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { ICurrentProcess, IPathUtils } from '../../../common/types'; -import { EnvironmentVariables, IEnvironmentVariablesService } from '../../../common/variables/types'; -import { LaunchRequestArguments } from '../../types'; - -export class DebugClientHelper { - constructor(private envParser: IEnvironmentVariablesService, private pathUtils: IPathUtils, - private process: ICurrentProcess) { } - public async getEnvironmentVariables(args: LaunchRequestArguments): Promise<EnvironmentVariables> { - const pathVariableName = this.pathUtils.getPathVariableName(); - - // Merge variables from both .env file and env json variables. - const envFileVars = await this.envParser.parseFile(args.envFile); - // tslint:disable-next-line:no-any - const debugLaunchEnvVars: {[key: string]: string} = (args.env && Object.keys(args.env).length > 0) ? { ...args.env } as any : {} as any; - const env = envFileVars ? { ...envFileVars! } : {}; - this.envParser.mergeVariables(debugLaunchEnvVars, env); - - // Append the PYTHONPATH and PATH variables. - this.envParser.appendPath(env, debugLaunchEnvVars[pathVariableName]); - this.envParser.appendPythonPath(env, debugLaunchEnvVars.PYTHONPATH); - - if (typeof env[pathVariableName] === 'string' && env[pathVariableName]!.length > 0) { - // Now merge this path with the current system path. - // We need to do this to ensure the PATH variable always has the system PATHs as well. - this.envParser.appendPath(env, this.process.env[pathVariableName]!); - } - if (typeof env.PYTHONPATH === 'string' && env.PYTHONPATH.length > 0) { - // We didn't have a value for PATH earlier and now we do. - // Now merge this path with the current system path. - // We need to do this to ensure the PATH variable always has the system PATHs as well. - this.envParser.appendPythonPath(env, this.process.env.PYTHONPATH!); - } - - if (typeof args.console !== 'string' || args.console === 'none') { - // For debugging, when not using any terminal, then we need to provide all env variables. - // As we're spawning the process, we need to ensure all env variables are passed. - // Including those from the current process (i.e. everything, not just custom vars). - this.envParser.mergeVariables(this.process.env, env); - - if (env[pathVariableName] === undefined && typeof this.process.env[pathVariableName] === 'string') { - env[pathVariableName] = this.process.env[pathVariableName]; - } - if (env.PYTHONPATH === undefined && typeof this.process.env.PYTHONPATH === 'string') { - env.PYTHONPATH = this.process.env.PYTHONPATH; - } - } - - if (!env.hasOwnProperty('PYTHONIOENCODING')) { - env.PYTHONIOENCODING = 'UTF-8'; - } - if (!env.hasOwnProperty('PYTHONUNBUFFERED')) { - env.PYTHONUNBUFFERED = '1'; - } - - if (args.gevent) { - env.GEVENT_SUPPORT = 'True'; // this is read in pydevd_constants.py - } - - return env; - } -} diff --git a/src/client/debugger/debugAdapter/DebugClients/launcherProvider.ts b/src/client/debugger/debugAdapter/DebugClients/launcherProvider.ts deleted file mode 100644 index bc4b94af90e7..000000000000 --- a/src/client/debugger/debugAdapter/DebugClients/launcherProvider.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-classes-per-file - -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../../common/constants'; -import { IDebugLauncherScriptProvider, IRemoteDebugLauncherScriptProvider, LocalDebugOptions, RemoteDebugOptions } from '../types'; - -const script = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'ptvsd_launcher.py'); -export class NoDebugLauncherScriptProvider implements IDebugLauncherScriptProvider<LocalDebugOptions> { - public getLauncherArgs(options: LocalDebugOptions): string[] { - const customDebugger = options.customDebugger ? '--custom' : '--default'; - return [script, customDebugger, '--nodebug', '--client', '--host', options.host, '--port', options.port.toString()]; - } -} - -export class DebuggerLauncherScriptProvider implements IDebugLauncherScriptProvider<LocalDebugOptions> { - public getLauncherArgs(options: LocalDebugOptions): string[] { - const customDebugger = options.customDebugger ? '--custom' : '--default'; - return [script, customDebugger, '--client', '--host', options.host, '--port', options.port.toString()]; - } -} - -export class RemoteDebuggerLauncherScriptProvider implements IRemoteDebugLauncherScriptProvider { - public getLauncherArgs(options: RemoteDebugOptions): string[] { - const waitArgs = options.waitUntilDebuggerAttaches ? ['--wait'] : []; - return [script, '--default', '--host', options.host, '--port', options.port.toString()].concat(waitArgs); - } -} diff --git a/src/client/debugger/debugAdapter/DebugClients/localDebugClientV2.ts b/src/client/debugger/debugAdapter/DebugClients/localDebugClientV2.ts deleted file mode 100644 index 63e0692fe830..000000000000 --- a/src/client/debugger/debugAdapter/DebugClients/localDebugClientV2.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DebugSession } from 'vscode-debugadapter'; -import { LaunchRequestArguments } from '../../types'; -import { ILocalDebugLauncherScriptProvider } from '../types'; -import { LocalDebugClient } from './LocalDebugClient'; - -export class LocalDebugClientV2 extends LocalDebugClient { - constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: ILocalDebugLauncherScriptProvider) { - super(args, debugSession, canLaunchTerminal, launcherScriptProvider); - } - protected buildDebugArguments(cwd: string, debugPort: number): string[] { - return this.launcherScriptProvider.getLauncherArgs({ host: 'localhost', port: debugPort, customDebugger: this.args.customDebugger }); - } - protected buildStandardArguments() { - const programArgs = Array.isArray(this.args.args) && this.args.args.length > 0 ? this.args.args : []; - if (typeof this.args.module === 'string' && this.args.module.length > 0) { - return ['-m', this.args.module, ...programArgs]; - } - if (this.args.program && this.args.program.length > 0) { - return [this.args.program, ...programArgs]; - } - return programArgs; - } -} diff --git a/src/client/debugger/debugAdapter/DebugClients/nonDebugClientV2.ts b/src/client/debugger/debugAdapter/DebugClients/nonDebugClientV2.ts deleted file mode 100644 index 5ac1c05eb2f0..000000000000 --- a/src/client/debugger/debugAdapter/DebugClients/nonDebugClientV2.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { ChildProcess } from 'child_process'; -import { DebugSession } from 'vscode-debugadapter'; -import { LaunchRequestArguments } from '../../types'; -import { ILocalDebugLauncherScriptProvider } from '../types'; -import { DebugType } from './DebugClient'; -import { LocalDebugClientV2 } from './localDebugClientV2'; - -export class NonDebugClientV2 extends LocalDebugClientV2 { - constructor(args: LaunchRequestArguments, debugSession: DebugSession, canLaunchTerminal: boolean, launcherScriptProvider: ILocalDebugLauncherScriptProvider) { - super(args, debugSession, canLaunchTerminal, launcherScriptProvider); - } - - public get DebugType(): DebugType { - return DebugType.RunLocal; - } - - public Stop() { - super.Stop(); - if (this.pyProc) { - try { - this.pyProc!.kill(); - // tslint:disable-next-line:no-empty - } catch { } - this.pyProc = undefined; - } - } - protected handleProcessOutput(proc: ChildProcess, _failedToLaunch: (error: Error | string | Buffer) => void) { - // Do nothing - } -} diff --git a/src/client/debugger/debugAdapter/DebugServers/BaseDebugServer.ts b/src/client/debugger/debugAdapter/DebugServers/BaseDebugServer.ts deleted file mode 100644 index 39c66a307007..000000000000 --- a/src/client/debugger/debugAdapter/DebugServers/BaseDebugServer.ts +++ /dev/null @@ -1,37 +0,0 @@ -// tslint:disable:quotemark ordered-imports no-any no-empty -'use strict'; - -import { DebugSession } from 'vscode-debugadapter'; -import { IDebugServer } from '../Common/Contracts'; -import { EventEmitter } from 'events'; -import { Socket } from 'net'; -import { Deferred, createDeferred } from '../../../common/utils/async'; - -export abstract class BaseDebugServer extends EventEmitter { - protected clientSocket: Deferred<Socket>; - public get client(): Promise<Socket> { - return this.clientSocket.promise; - } - protected debugSession: DebugSession; - - protected isRunning: boolean = false; - public get IsRunning(): boolean { - if (this.isRunning === undefined) { - return false; - } - return this.isRunning; - } - protected debugClientConnected: Deferred<boolean>; - public get DebugClientConnected(): Promise<boolean> { - return this.debugClientConnected.promise; - } - constructor(debugSession: DebugSession) { - super(); - this.debugSession = debugSession; - this.debugClientConnected = createDeferred<boolean>(); - this.clientSocket = createDeferred<Socket>(); - } - - public abstract Start(): Promise<IDebugServer>; - public abstract Stop(); -} diff --git a/src/client/debugger/debugAdapter/DebugServers/LocalDebugServerV2.ts b/src/client/debugger/debugAdapter/DebugServers/LocalDebugServerV2.ts deleted file mode 100644 index 0b024e594362..000000000000 --- a/src/client/debugger/debugAdapter/DebugServers/LocalDebugServerV2.ts +++ /dev/null @@ -1,49 +0,0 @@ - -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as net from 'net'; -import { DebugSession } from 'vscode-debugadapter'; -import { ISocketServer } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; -import { IServiceContainer } from '../../../ioc/types'; -import { LaunchRequestArguments } from '../../types'; -import { IDebugServer } from '../Common/Contracts'; -import { BaseDebugServer } from './BaseDebugServer'; - -export class LocalDebugServerV2 extends BaseDebugServer { - private socketServer?: ISocketServer; - - constructor(debugSession: DebugSession, private args: LaunchRequestArguments, private serviceContainer: IServiceContainer) { - super(debugSession); - this.clientSocket = createDeferred<net.Socket>(); - } - - public Stop() { - if (this.socketServer) { - try { - this.socketServer.dispose(); - // tslint:disable-next-line:no-empty - } catch { } - this.socketServer = undefined; - } - } - - public async Start(): Promise<IDebugServer> { - const host = typeof this.args.host === 'string' && this.args.host.trim().length > 0 ? this.args.host!.trim() : 'localhost'; - const socketServer = this.socketServer = this.serviceContainer.get<ISocketServer>(ISocketServer); - const port = await socketServer.Start({ port: this.args.port, host }); - socketServer.client.then(socket => { - // This is required to prevent the launcher from aborting if the PTVSD process spits out any errors in stderr stream. - this.isRunning = true; - this.debugClientConnected.resolve(true); - this.clientSocket.resolve(socket); - }).catch(ex => { - this.debugClientConnected.reject(ex); - this.clientSocket.reject(ex); - }); - return { port, host }; - } -} diff --git a/src/client/debugger/debugAdapter/DebugServers/RemoteDebugServerv2.ts b/src/client/debugger/debugAdapter/DebugServers/RemoteDebugServerv2.ts deleted file mode 100644 index 765e83aa8fed..000000000000 --- a/src/client/debugger/debugAdapter/DebugServers/RemoteDebugServerv2.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Socket } from 'net'; -import { DebugSession } from 'vscode-debugadapter'; -import { AttachRequestArguments } from '../../types'; -import { IDebugServer } from '../Common/Contracts'; -import { BaseDebugServer } from './BaseDebugServer'; - -export class RemoteDebugServerV2 extends BaseDebugServer { - private args: AttachRequestArguments; - private socket?: Socket; - constructor(debugSession: DebugSession, args: AttachRequestArguments) { - super(debugSession); - this.args = args; - } - - public Stop() { - if (this.socket) { - this.socket.destroy(); - } - } - public Start(): Promise<IDebugServer> { - return new Promise<IDebugServer>((resolve, reject) => { - const port = this.args.port!; - const options = { port }; - if (typeof this.args.host === 'string' && this.args.host.length > 0) { - // tslint:disable-next-line:no-any - (<any>options).host = this.args.host; - } - try { - let connected = false; - const socket = new Socket(); - socket.on('error', ex => { - if (connected) { - return; - } - reject(ex); - }); - socket.connect(options, () => { - connected = true; - this.socket = socket; - this.clientSocket.resolve(socket); - resolve(options); - }); - } catch (ex) { - reject(ex); - } - }); - } -} diff --git a/src/client/debugger/debugAdapter/main.ts b/src/client/debugger/debugAdapter/main.ts deleted file mode 100644 index bbc118d0178c..000000000000 --- a/src/client/debugger/debugAdapter/main.ts +++ /dev/null @@ -1,508 +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-empty no-require-imports no-var-requires - -if ((Reflect as any).metadata === undefined) { - require('reflect-metadata'); -} - -import { Socket } from 'net'; -import { EOL } from 'os'; -import * as path from 'path'; -import { PassThrough, Writable } from 'stream'; -import { Disposable } from 'vscode'; -import { DebugSession, ErrorDestination, Event, logger, OutputEvent, Response, TerminatedEvent } from 'vscode-debugadapter'; -import { LogLevel } from 'vscode-debugadapter/lib/logger'; -import { DebugProtocol } from 'vscode-debugprotocol'; -import { EXTENSION_ROOT_DIR } from '../../common/constants'; -import '../../common/extensions'; -import { isNotInstalledError } from '../../common/helpers'; -import { IFileSystem } from '../../common/platform/types'; -import { ICurrentProcess } from '../../common/types'; -import { createDeferred, Deferred, sleep } from '../../common/utils/async'; -import { noop } from '../../common/utils/misc'; -import { IServiceContainer } from '../../ioc/types'; -import { AttachRequestArguments, LaunchRequestArguments } from '../types'; -import { CreateAttachDebugClient, CreateLaunchDebugClient } from './DebugClients/DebugFactory'; -import { BaseDebugServer } from './DebugServers/BaseDebugServer'; -import { initializeIoc } from './serviceRegistry'; -import { IDebugStreamProvider, IProtocolLogger, IProtocolMessageWriter, IProtocolParser } from './types'; -const killProcessTree = require('tree-kill'); - -const DEBUGGER_CONNECT_TIMEOUT = 20000; -const MIN_DEBUGGER_CONNECT_TIMEOUT = 5000; - -/** - * Primary purpose of this class is to perform the handshake with VS Code and launch PTVSD process. - * I.e. it communicate with VS Code before PTVSD gets into the picture, once PTVSD is launched, PTVSD will talk directly to VS Code. - * We're re-using DebugSession so we don't have to handle request/response ourselves. - * @export - * @class PythonDebugger - * @extends {DebugSession} - */ -export class PythonDebugger extends DebugSession { - public debugServer?: BaseDebugServer; - public client = createDeferred<Socket>(); - private supportsRunInTerminalRequest: boolean = false; - constructor(private readonly serviceContainer: IServiceContainer) { - super(false); - } - public shutdown(): void { - if (this.debugServer) { - this.debugServer.Stop(); - this.debugServer = undefined; - } - super.shutdown(); - } - protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { - const body = response.body!; - - body.supportsExceptionInfoRequest = true; - body.supportsConfigurationDoneRequest = true; - body.supportsConditionalBreakpoints = true; - body.supportsSetVariable = true; - body.supportsExceptionOptions = true; - body.supportsEvaluateForHovers = true; - body.supportsModulesRequest = true; - body.supportsValueFormattingOptions = true; - body.supportsHitConditionalBreakpoints = true; - body.supportsSetExpression = true; - body.supportsLogPoints = true; - body.supportTerminateDebuggee = true; - body.supportsCompletionsRequest = true; - body.exceptionBreakpointFilters = [ - { - filter: 'raised', - label: 'Raised Exceptions', - default: false - }, - { - filter: 'uncaught', - label: 'Uncaught Exceptions', - default: true - } - ]; - if (typeof args.supportsRunInTerminalRequest === 'boolean') { - this.supportsRunInTerminalRequest = args.supportsRunInTerminalRequest; - } - this.sendResponse(response); - } - protected attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments): void { - const launcher = CreateAttachDebugClient(args, this); - this.debugServer = launcher.CreateDebugServer(this.serviceContainer); - this.debugServer!.Start() - .then(() => this.emit('debugger_attached')) - .catch(ex => { - logger.error('Attach failed'); - logger.error(`${ex}, ${ex.name}, ${ex.message}, ${ex.stack}`); - const message = this.getUserFriendlyAttachErrorMessage(ex) || 'Attach Failed'; - this.sendErrorResponse(response, { format: message, id: 1 }, undefined, undefined, ErrorDestination.User); - }); - - } - protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { - const fs = this.serviceContainer.get<IFileSystem>(IFileSystem); - if ((typeof args.module !== 'string' || args.module.length === 0) && args.program && !fs.fileExistsSync(args.program)) { - return this.sendErrorResponse(response, { format: `File does not exist. "${args.program}"`, id: 1 }, undefined, undefined, ErrorDestination.User); - } - - this.launchPTVSD(args) - .then(() => this.waitForPTVSDToConnect(args)) - .then(() => this.emit('debugger_launched')) - .catch(ex => { - const message = this.getUserFriendlyLaunchErrorMessage(args, ex) || 'Debug Error'; - this.sendErrorResponse(response, { format: message, id: 1 }, undefined, undefined, ErrorDestination.User); - }); - } - private async launchPTVSD(args: LaunchRequestArguments) { - const launcher = CreateLaunchDebugClient(args, this, this.supportsRunInTerminalRequest); - this.debugServer = launcher.CreateDebugServer(this.serviceContainer); - const serverInfo = await this.debugServer!.Start(); - return launcher.LaunchApplicationToDebug(serverInfo); - } - private async waitForPTVSDToConnect(args: LaunchRequestArguments) { - return new Promise<void>(async (resolve, reject) => { - let rejected = false; - const duration = this.getConnectionTimeout(args); - const timeout = setTimeout(() => { - rejected = true; - reject(new Error('Timeout waiting for debugger connection')); - }, duration); - - try { - await this.debugServer!.client; - timeout.unref(); - if (!rejected) { - resolve(); - } - } catch (ex) { - reject(ex); - } - }); - } - private getConnectionTimeout(args: LaunchRequestArguments) { - // The timeout can be overridden, but won't be documented unless we see the need for it. - // This is just a fail safe mechanism, if the current timeout isn't enough (let study the current behaviour before exposing this setting). - const connectionTimeout = typeof (args as any).timeout === 'number' ? (args as any).timeout as number : DEBUGGER_CONNECT_TIMEOUT; - return Math.max(connectionTimeout, MIN_DEBUGGER_CONNECT_TIMEOUT); - } - private getUserFriendlyLaunchErrorMessage(launchArgs: LaunchRequestArguments, error: any): string | undefined { - if (!error) { - return; - } - const errorMsg = typeof error === 'string' ? error : ((error.message && error.message.length > 0) ? error.message : ''); - if (isNotInstalledError(error)) { - return `Failed to launch the Python Process, please validate the path '${launchArgs.pythonPath}'`; - } else { - return errorMsg; - } - } - private getUserFriendlyAttachErrorMessage(error: any): string | undefined { - if (!error) { - return; - } - if (error.code === 'ECONNREFUSED' || error.errno === 'ECONNREFUSED') { - return `Failed to attach (${error.message})`; - } else { - return typeof error === 'string' ? error : ((error.message && error.message.length > 0) ? error.message : ''); - } - } -} - -/** - * Glue that orchestrates communications between VS Code, PythonDebugger (DebugSession) and PTVSD. - * @class DebugManager - * @implements {Disposable} - */ -class DebugManager implements Disposable { - // #region VS Code debug Streams. - private inputStream!: NodeJS.ReadStream | Socket; - private outputStream!: NodeJS.WriteStream | Socket; - // #endregion - // #region Proxy Streams (used to listen in on the communications). - private readonly throughOutputStream: PassThrough; - private readonly throughInputStream: PassThrough; - // #endregion - // #region Streams used by the PythonDebug class (DebugSession). - private readonly debugSessionOutputStream: PassThrough; - private readonly debugSessionInputStream: PassThrough; - // #endregion - // #region Streams used to communicate with PTVSD. - private socket!: Socket; - // #endregion - private readonly inputProtocolParser: IProtocolParser; - private readonly outputProtocolParser: IProtocolParser; - private readonly protocolLogger: IProtocolLogger; - private readonly protocolMessageWriter: IProtocolMessageWriter; - private isServerMode: boolean = false; - private readonly disposables: Disposable[] = []; - private hasShutdown: boolean = false; - private debugSession?: PythonDebugger; - private ptvsdProcessId?: number; - private launchOrAttach?: 'launch' | 'attach'; - private terminatedEventSent: boolean = false; - private disconnectResponseSent: boolean = false; - private disconnectRequest?: DebugProtocol.DisconnectRequest; - private restart: boolean = false; - private readonly initializeRequestDeferred: Deferred<DebugProtocol.InitializeRequest>; - private get initializeRequest(): Promise<DebugProtocol.InitializeRequest> { - return this.initializeRequestDeferred.promise; - } - private readonly launchRequestDeferred: Deferred<DebugProtocol.LaunchRequest>; - private get launchRequest(): Promise<DebugProtocol.LaunchRequest> { - return this.launchRequestDeferred.promise; - } - - private readonly attachRequestDeferred: Deferred<DebugProtocol.AttachRequest>; - private get attachRequest(): Promise<DebugProtocol.AttachRequest> { - return this.attachRequestDeferred.promise; - } - - private set loggingEnabled(value: boolean) { - if (value) { - logger.setup(LogLevel.Verbose, true); - this.protocolLogger.setup(logger); - } - } - constructor(private readonly serviceContainer: IServiceContainer) { - this.throughInputStream = new PassThrough(); - this.throughOutputStream = new PassThrough(); - this.debugSessionOutputStream = new PassThrough(); - this.debugSessionInputStream = new PassThrough(); - - this.protocolMessageWriter = this.serviceContainer.get<IProtocolMessageWriter>(IProtocolMessageWriter); - - this.inputProtocolParser = this.serviceContainer.get<IProtocolParser>(IProtocolParser); - this.inputProtocolParser.connect(this.throughInputStream); - this.disposables.push(this.inputProtocolParser); - this.outputProtocolParser = this.serviceContainer.get<IProtocolParser>(IProtocolParser); - this.outputProtocolParser.connect(this.throughOutputStream); - this.disposables.push(this.outputProtocolParser); - - this.protocolLogger = this.serviceContainer.get<IProtocolLogger>(IProtocolLogger); - this.protocolLogger.connect(this.throughInputStream, this.throughOutputStream); - this.disposables.push(this.protocolLogger); - - this.initializeRequestDeferred = createDeferred<DebugProtocol.InitializeRequest>(); - this.launchRequestDeferred = createDeferred<DebugProtocol.LaunchRequest>(); - this.attachRequestDeferred = createDeferred<DebugProtocol.AttachRequest>(); - } - public dispose() { - logger.verbose('main dispose'); - this.shutdown().ignoreErrors(); - } - public async start() { - const debugStreamProvider = this.serviceContainer.get<IDebugStreamProvider>(IDebugStreamProvider); - const { input, output } = await debugStreamProvider.getInputAndOutputStreams(); - this.isServerMode = debugStreamProvider.useDebugSocketStream; - this.inputStream = input; - this.outputStream = output; - this.inputStream.pause(); - if (!this.isServerMode) { - const currentProcess = this.serviceContainer.get<ICurrentProcess>(ICurrentProcess); - currentProcess.on('SIGTERM', () => { - if (!this.restart) { - this.shutdown().ignoreErrors(); - } - }); - } - this.interceptProtocolMessages(); - this.startDebugSession(); - } - /** - * Do not put any delays in here expecting VSC to receive messages. VSC could disconnect earlier (PTVSD #128). - * If any delays are necessary, add them prior to calling this method. - * If the program is forcefully terminated (e.g. killing terminal), we handle socket.on('error') or socket.on('close'), - * Under such circumstances, we need to send the terminated event asap (could be because VSC might be getting an error at its end due to piped stream being closed). - * @private - * @memberof DebugManager - */ - // tslint:disable-next-line:cyclomatic-complexity - private shutdown = async () => { - logger.verbose('check and shutdown'); - if (this.hasShutdown) { - return; - } - this.hasShutdown = true; - logger.verbose('shutdown'); - - if (!this.terminatedEventSent && !this.restart) { - // Possible PTVSD died before sending message back. - try { - logger.verbose('Sending Terminated Event'); - this.sendMessage(new TerminatedEvent(), this.outputStream); - } catch (err) { - const message = `Error in sending Terminated Event: ${err && err.message ? err.message : err.toString()}`; - const details = [message, err && err.name ? err.name : '', err && err.stack ? err.stack : ''].join(EOL); - logger.error(`${message}${EOL}${details}`); - } - this.terminatedEventSent = true; - } - - if (!this.disconnectResponseSent && this.restart && this.disconnectRequest) { - // This is a work around for PTVSD bug, else this entire block is unnecessary. - try { - logger.verbose('Sending Disconnect Response'); - this.sendMessage(new Response(this.disconnectRequest, ''), this.outputStream); - } catch (err) { - const message = `Error in sending Disconnect Response: ${err && err.message ? err.message : err.toString()}`; - const details = [message, err && err.name ? err.name : '', err && err.stack ? err.stack : ''].join(EOL); - logger.error(`${message}${EOL}${details}`); - } - this.disconnectResponseSent = true; - } - - if (this.launchOrAttach === 'launch' && this.ptvsdProcessId) { - logger.verbose('killing process'); - try { - // 1. Wait for some time, its possible the program has run to completion. - // We need to wait till the process exits (else the message `Terminated: 15` gets printed onto the screen). - // 2. Also, its possible we manually sent the `Terminated` event above. - // Hence we need to wait till VSC receives the above event. - await sleep(100); - logger.verbose('Kill process now'); - killProcessTree(this.ptvsdProcessId!); - } catch { } - this.ptvsdProcessId = undefined; - } - - if (!this.restart) { - if (this.debugSession) { - logger.verbose('Shutting down debug session'); - this.debugSession.shutdown(); - } - - logger.verbose('disposing'); - await sleep(100); - // Dispose last, we don't want to dispose the protocol loggers too early. - this.disposables.forEach(disposable => disposable.dispose()); - } - } - private sendMessage(message: DebugProtocol.ProtocolMessage, outputStream: Socket | PassThrough | NodeJS.WriteStream): void { - this.protocolMessageWriter.write(outputStream, message); - this.protocolMessageWriter.write(this.throughOutputStream, message); - } - private startDebugSession() { - this.debugSession = new PythonDebugger(this.serviceContainer); - this.debugSession.setRunAsServer(this.isServerMode); - - this.debugSession.once('debugger_attached', this.connectVSCodeToPTVSD); - this.debugSession.once('debugger_launched', this.connectVSCodeToPTVSD); - - this.debugSessionOutputStream.pipe(this.throughOutputStream); - this.debugSessionOutputStream.pipe(this.outputStream); - - // Start handling requests in the session instance. - // The session (PythonDebugger class) will only perform the bootstrapping (launching of PTVSD). - this.inputStream.pipe(this.throughInputStream); - this.inputStream.pipe(this.debugSessionInputStream); - - this.debugSession.start(this.debugSessionInputStream, this.debugSessionOutputStream); - } - private interceptProtocolMessages() { - // Keep track of the initialize and launch requests, we'll need to re-send these to ptvsd, for bootstrapping. - this.inputProtocolParser.once('request_initialize', this.onRequestInitialize); - this.inputProtocolParser.once('request_launch', this.onRequestLaunch); - this.inputProtocolParser.once('request_attach', this.onRequestAttach); - this.inputProtocolParser.once('request_disconnect', this.onRequestDisconnect); - - this.outputProtocolParser.once('event_terminated', this.onEventTerminated); - this.outputProtocolParser.once('response_disconnect', this.onResponseDisconnect); - } - /** - * Connect PTVSD socket to VS Code. - * This allows PTVSD to communicate directly with VS Code. - * @private - * @memberof DebugManager - */ - private connectVSCodeToPTVSD = async (response: DebugProtocol.AttachResponse | DebugProtocol.LaunchResponse) => { - const attachOrLaunchRequest = await (this.launchOrAttach === 'attach' ? this.attachRequest : this.launchRequest); - // By now we're connected to the client. - this.socket = await this.debugSession!.debugServer!.client; - - // We need to handle both end and error, sometimes the socket will error out without ending (if debugee is killed). - // Note, we need a handler for the error event, else nodejs complains when socket gets closed and there are no error handlers. - this.socket.on('end', () => { - logger.verbose('Socket End'); - this.shutdown().ignoreErrors(); - }); - this.socket.on('error', () => { - logger.verbose('Socket Error'); - this.shutdown().ignoreErrors(); - }); - // Keep track of processid for killing it. - if (this.launchOrAttach === 'launch') { - const debugSoketProtocolParser = this.serviceContainer.get<IProtocolParser>(IProtocolParser); - debugSoketProtocolParser.connect(this.socket); - debugSoketProtocolParser.once('event_process', (proc: DebugProtocol.ProcessEvent) => { - this.ptvsdProcessId = proc.body.systemProcessId; - }); - } - - // Get ready for PTVSD to communicate directly with VS Code. - (this.inputStream as any as NodeJS.ReadStream).unpipe<Writable>(this.debugSessionInputStream); - this.debugSessionOutputStream.unpipe(this.outputStream); - - // Do not pipe. When restarting the debugger, the socket gets closed, - // In which case, VSC will see this and shutdown the debugger completely. - (this.inputStream as any as NodeJS.ReadStream).on('data', data => { - this.socket.write(data); - }); - this.socket.on('data', (data: string | Buffer) => { - this.throughOutputStream.write(data); - this.outputStream.write(data as string); - }); - - // Send the launch/attach request to PTVSD and wait for it to reply back. - this.sendMessage(attachOrLaunchRequest, this.socket); - - // Send the initialize request and wait for it to reply back with the initialized event - this.sendMessage(await this.initializeRequest, this.socket); - } - private onRequestInitialize = (request: DebugProtocol.InitializeRequest) => { - this.hasShutdown = false; - this.terminatedEventSent = false; - this.disconnectResponseSent = false; - this.restart = false; - this.disconnectRequest = undefined; - this.initializeRequestDeferred.resolve(request); - } - private onRequestLaunch = (request: DebugProtocol.LaunchRequest) => { - this.launchOrAttach = 'launch'; - this.loggingEnabled = (request.arguments as LaunchRequestArguments).logToFile === true; - this.launchRequestDeferred.resolve(request); - } - private onRequestAttach = (request: DebugProtocol.AttachRequest) => { - this.launchOrAttach = 'attach'; - this.loggingEnabled = (request.arguments as AttachRequestArguments).logToFile === true; - this.attachRequestDeferred.resolve(request); - } - private onRequestDisconnect = (request: DebugProtocol.DisconnectRequest) => { - this.disconnectRequest = request; - if (this.launchOrAttach === 'attach') { - return; - } - const args = request.arguments as { restart: boolean } | undefined; - if (args && args.restart) { - this.restart = true; - } - - // When VS Code sends a disconnect request, PTVSD replies back with a response. - // Wait for sometime, untill the messages are sent out (remember, we're just intercepting streams here). - setTimeout(this.shutdown, 500); - } - private onEventTerminated = async () => { - logger.verbose('onEventTerminated'); - this.terminatedEventSent = true; - // Wait for sometime, untill the messages are sent out (remember, we're just intercepting streams here). - setTimeout(this.shutdown, 300); - } - private onResponseDisconnect = async () => { - this.disconnectResponseSent = true; - logger.verbose('onResponseDisconnect'); - // When VS Code sends a disconnect request, PTVSD replies back with a response, but its upto us to kill the process. - // Wait for sometime, untill the messages are sent out (remember, we're just intercepting streams here). - // Also its possible PTVSD might run to completion. - setTimeout(this.shutdown, 100); - } -} - -async function startDebugger() { - logger.init(noop, path.join(EXTENSION_ROOT_DIR, `debug${process.pid}.log`)); - const serviceContainer = initializeIoc(); - const protocolMessageWriter = serviceContainer.get<IProtocolMessageWriter>(IProtocolMessageWriter); - try { - // debugger; - const debugManager = new DebugManager(serviceContainer); - await debugManager.start(); - } catch (err) { - const message = `Debugger Error: ${err && err.message ? err.message : err.toString()}`; - const details = [message, err && err.name ? err.name : '', err && err.stack ? err.stack : ''].join(EOL); - logger.error(`${message}${EOL}${details}`); - - // Notify the user. - protocolMessageWriter.write(process.stdout, new Event('error', message)); - protocolMessageWriter.write(process.stdout, new OutputEvent(`${message}${EOL}${details}`, 'stderr')); - } -} - -process.stdin.on('error', () => { }); -process.stdout.on('error', () => { }); -process.stderr.on('error', () => { }); - -process.on('uncaughtException', (err: Error) => { - logger.error(`Uncaught Exception: ${err && err.message ? err.message : ''}`); - logger.error(err && err.name ? err.name : ''); - logger.error(err && err.stack ? err.stack : ''); - // Catch all, incase we have string exceptions being raised. - logger.error(err ? err.toString() : ''); - // Wait for 1 second before we die, we need to ensure errors are written to the log file. - setTimeout(() => process.exit(-1), 100); -}); - -startDebugger().catch(ex => { - // Not necessary except for debugging and to kill linter warning about unhandled promises. -}); diff --git a/src/client/debugger/debugAdapter/serviceRegistry.ts b/src/client/debugger/debugAdapter/serviceRegistry.ts deleted file mode 100644 index dc871251dd49..000000000000 --- a/src/client/debugger/debugAdapter/serviceRegistry.ts +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Container } from 'inversify'; -import { SocketServer } from '../../common/net/socket/socketServer'; -import { FileSystem } from '../../common/platform/fileSystem'; -import { PlatformService } from '../../common/platform/platformService'; -import { IFileSystem, IPlatformService } from '../../common/platform/types'; -import { CurrentProcess } from '../../common/process/currentProcess'; -import { BufferDecoder } from '../../common/process/decoder'; -import { IBufferDecoder, IProcessServiceFactory } from '../../common/process/types'; -import { ICurrentProcess, ISocketServer } from '../../common/types'; -import { ServiceContainer } from '../../ioc/container'; -import { ServiceManager } from '../../ioc/serviceManager'; -import { IServiceContainer, IServiceManager } from '../../ioc/types'; -import { DebugStreamProvider } from './Common/debugStreamProvider'; -import { DebuggerProcessServiceFactory } from './Common/processServiceFactory'; -import { ProtocolLogger } from './Common/protocolLogger'; -import { ProtocolParser } from './Common/protocolParser'; -import { ProtocolMessageWriter } from './Common/protocolWriter'; -import { IDebugStreamProvider, IProtocolLogger, IProtocolMessageWriter, IProtocolParser } from './types'; - -export function initializeIoc(): IServiceContainer { - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - const serviceContainer = new ServiceContainer(cont); - serviceManager.addSingletonInstance<IServiceContainer>(IServiceContainer, serviceContainer); - registerTypes(serviceManager); - return serviceContainer; -} - -function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess); - serviceManager.addSingleton<IDebugStreamProvider>(IDebugStreamProvider, DebugStreamProvider); - serviceManager.addSingleton<IProtocolLogger>(IProtocolLogger, ProtocolLogger); - serviceManager.add<IProtocolParser>(IProtocolParser, ProtocolParser); - serviceManager.addSingleton<IFileSystem>(IFileSystem, FileSystem); - serviceManager.addSingleton<IPlatformService>(IPlatformService, PlatformService); - serviceManager.addSingleton<ISocketServer>(ISocketServer, SocketServer); - serviceManager.addSingleton<IProtocolMessageWriter>(IProtocolMessageWriter, ProtocolMessageWriter); - serviceManager.addSingleton<IBufferDecoder>(IBufferDecoder, BufferDecoder); - serviceManager.addSingleton<IProcessServiceFactory>(IProcessServiceFactory, DebuggerProcessServiceFactory); -} diff --git a/src/client/debugger/debugAdapter/types.ts b/src/client/debugger/debugAdapter/types.ts deleted file mode 100644 index 24f20e7d289a..000000000000 --- a/src/client/debugger/debugAdapter/types.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Socket } from 'net'; -import { Readable } from 'stream'; -import { Disposable } from 'vscode'; -import { Logger } from 'vscode-debugadapter'; -import { Message } from 'vscode-debugadapter/lib/messages'; - -export type LocalDebugOptions = { port: number; host: string; customDebugger?: boolean }; -export type RemoteDebugOptions = LocalDebugOptions & { waitUntilDebuggerAttaches: boolean }; - -export interface IDebugLauncherScriptProvider<T> { - getLauncherArgs(options: T): string[]; -} - -export interface ILocalDebugLauncherScriptProvider extends IDebugLauncherScriptProvider<LocalDebugOptions> { - getLauncherArgs(options: LocalDebugOptions): string[]; -} - -export interface IRemoteDebugLauncherScriptProvider extends IDebugLauncherScriptProvider<RemoteDebugOptions> { -} - -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 const IProtocolLogger = Symbol('IProtocolLogger'); -export interface IProtocolLogger extends Disposable { - connect(inputStream: Readable, outputStream: Readable): void; - setup(logger: Logger.ILogger): void; -} - -export const IDebugStreamProvider = Symbol('IDebugStreamProvider'); -export interface IDebugStreamProvider { - readonly useDebugSocketStream: boolean; - getInputAndOutputStreams(): Promise<{ input: NodeJS.ReadStream | Socket; output: NodeJS.WriteStream | Socket }>; -} - -export const IProtocolMessageWriter = Symbol('IProtocolMessageWriter'); -export interface IProtocolMessageWriter { - write(stream: Socket | NodeJS.WriteStream, message: Message): void; -} diff --git a/src/client/debugger/extension/adapter/activator.ts b/src/client/debugger/extension/adapter/activator.ts new file mode 100644 index 000000000000..999c00366ed6 --- /dev/null +++ b/src/client/debugger/extension/adapter/activator.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// 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 { 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, + ) {} + public async activate(): Promise<void> { + this.attachProcessProviderFactory.registerCommands(); + + this.disposables.push( + this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debugSessionLoggingFactory), + ); + this.disposables.push( + this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debuggerPromptFactory), + ); + + this.disposables.push( + this.debugService.registerDebugAdapterDescriptorFactory(DebuggerTypeName, this.descriptorFactory), + ); + this.disposables.push( + 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 new file mode 100644 index 000000000000..edef16368dc0 --- /dev/null +++ b/src/client/debugger/extension/adapter/factory.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { + DebugAdapterDescriptor, + DebugAdapterExecutable, + DebugAdapterServer, + DebugSession, + l10n, + WorkspaceFolder, +} from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { IInterpreterService } from '../../../interpreter/contracts'; +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(IPersistentStateFactory) private persistentState: IPersistentStateFactory, + ) {} + + public async createDebugAdapterDescriptor( + session: DebugSession, + _executable: DebugAdapterExecutable | undefined, + ): Promise<DebugAdapterDescriptor> { + const configuration = session.configuration as LaunchRequestArguments | AttachRequestArguments; + + // There are four distinct scenarios here: + // + // 1. "launch"; + // 2. "attach" with "processId"; + // 3. "attach" with "listen"; + // 4. "attach" with "connect" (or legacy "host"/"port"); + // + // For the first three, we want to spawn the debug adapter directly. + // For the last one, the adapter is already listening on the specified socket. + // When "debugServer" is used, the standard adapter factory takes care of it - no need to check here. + + if (configuration.request === 'attach') { + if (configuration.connect !== undefined) { + 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 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) { + 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 args = command.concat([debuggerAdapterPathToUse, ...logArgs]); + traceLog(`DAP Server launched with command: ${executable} ${args.join(' ')}`); + return new DebugAdapterExecutable(executable, args); + } + + // Unlikely scenario. + throw new Error('Debug Adapter Executable not provided'); + } + + /** + * Get the python executable used to launch the Python Debug Adapter. + * In the case of `attach` scenarios, just use the workspace interpreter, else first available one. + * It is unlike user won't have a Python interpreter + * + * @private + * @param {(LaunchRequestArguments | AttachRequestArguments)} configuration + * @param {WorkspaceFolder} [workspaceFolder] + * @returns {Promise<string>} Path to the python interpreter for this workspace. + * @memberof DebugAdapterDescriptorFactory + */ + private async getDebugAdapterPython( + configuration: LaunchRequestArguments | AttachRequestArguments, + workspaceFolder?: WorkspaceFolder, + ): Promise<string[]> { + 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 this.getExecutableCommand(interpreter); + } + + 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 []; + } + + traceVerbose(`Picking first available interpreter to launch the DA '${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<string[]> { + 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 []; + } + + /** + * Notify user about the requirement for Python. + * Unlikely scenario, as ex expect users to have Python in order to use the extension. + * However it is possible to ignore the warnings and continue using the extension. + * + * @private + * @memberof DebugAdapterDescriptorFactory + */ + private async notifySelectInterpreter() { + await 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 new file mode 100644 index 000000000000..907b895170c6 --- /dev/null +++ b/src/client/debugger/extension/adapter/logging.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { + DebugAdapterTracker, + DebugAdapterTrackerFactory, + DebugConfiguration, + DebugSession, + ProviderResult, +} from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +import { IFileSystem, WriteStream } from '../../../common/platform/types'; +import { StopWatch } from '../../../common/utils/stopWatch'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; + +class DebugSessionLoggingTracker implements DebugAdapterTracker { + private readonly enabled: boolean = false; + private stream?: WriteStream; + private timer = new StopWatch(); + + constructor(private readonly session: DebugSession, fileSystem: IFileSystem) { + this.enabled = this.session.configuration.logToFile as boolean; + if (this.enabled) { + const fileName = `debugger.vscode_${this.session.id}.log`; + this.stream = fileSystem.createWriteStream(path.join(EXTENSION_ROOT_DIR, fileName)); + } + } + + public onWillStartSession() { + this.timer.reset(); + this.log(`Starting Session:\n${this.stringify(this.session.configuration)}\n`); + } + + public onWillReceiveMessage(message: DebugProtocol.Message) { + this.log(`Client --> Adapter:\n${this.stringify(message)}\n`); + } + + public onDidSendMessage(message: DebugProtocol.Message) { + this.log(`Client <-- Adapter:\n${this.stringify(message)}\n`); + } + + public onWillStopSession() { + this.log('Stopping Session\n'); + } + + public onError(error: Error) { + this.log(`Error:\n${this.stringify(error)}\n`); + } + + public onExit(code: number | undefined, signal: string | undefined) { + this.log(`Exit:\nExit-Code: ${code ? code : 0}\nSignal: ${signal ? signal : 'none'}\n`); + this.stream?.close(); + } + + private log(message: string) { + if (this.enabled) { + this.stream!.write(`${this.timer.elapsedTime} ${message}`); // NOSONAR + } + } + + private stringify(data: DebugProtocol.Message | Error | DebugConfiguration) { + return JSON.stringify(data, null, 4); + } +} + +@injectable() +export class DebugSessionLoggingFactory implements DebugAdapterTrackerFactory { + constructor(@inject(IFileSystem) private readonly fileSystem: IFileSystem) {} + + public createDebugAdapterTracker(session: DebugSession): ProviderResult<DebugAdapterTracker> { + return new DebugSessionLoggingTracker(session, this.fileSystem); + } +} diff --git a/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts b/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts new file mode 100644 index 000000000000..04117e9838d1 --- /dev/null +++ b/src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; +import { injectable } from 'inversify'; +import { DebugAdapterTracker, DebugAdapterTrackerFactory, DebugSession, ProviderResult } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +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) {} + + public onDidSendMessage(message: DebugProtocol.ProtocolMessage) { + if (this.promptCheck.shouldShowPrompt() && this.isPtvsd(message)) { + const prompts = [Common.moreInfo]; + showInformationMessage(OutdatedDebugger.outdatedDebuggerMessage, ...prompts).then((selection) => { + if (selection === prompts[0]) { + launch('https://aka.ms/migrateToDebugpy'); + } + }); + } + } + + private isPtvsd(message: DebugProtocol.ProtocolMessage) { + if (message.type === 'event') { + const eventMessage = message as DebugProtocol.Event; + if (eventMessage.event === 'output') { + const outputMessage = eventMessage as DebugProtocol.OutputEvent; + if (outputMessage.body.category === 'telemetry') { + // debugpy sends telemetry as both ptvsd and debugpy. This was done to help with + // transition from ptvsd to debugpy while analyzing usage telemetry. + if ( + outputMessage.body.output === 'ptvsd' && + !outputMessage.body.data.packageVersion.startsWith('1') + ) { + this.promptCheck.setShowPrompt(false); + return true; + } + if (outputMessage.body.output === 'debugpy') { + this.promptCheck.setShowPrompt(false); + } + } + } + } + return false; + } +} + +class OutdatedDebuggerPromptState implements IPromptShowState { + private shouldShow: boolean = true; + public shouldShowPrompt(): boolean { + return this.shouldShow; + } + public setShowPrompt(show: boolean) { + this.shouldShow = show; + } +} + +@injectable() +export class OutdatedDebuggerPromptFactory implements DebugAdapterTrackerFactory { + private readonly promptCheck: OutdatedDebuggerPromptState; + constructor() { + this.promptCheck = new OutdatedDebuggerPromptState(); + } + public createDebugAdapterTracker(_session: DebugSession): ProviderResult<DebugAdapterTracker> { + return new OutdatedDebuggerPrompt(this.promptCheck); + } +} diff --git a/src/client/debugger/extension/adapter/remoteLaunchers.ts b/src/client/debugger/extension/adapter/remoteLaunchers.ts new file mode 100644 index 000000000000..f68f747a8a8c --- /dev/null +++ b/src/client/debugger/extension/adapter/remoteLaunchers.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import '../../../common/extensions'; +import { getDebugpyPath } from '../../pythonDebugger'; + +type RemoteDebugOptions = { + host: string; + port: number; + waitUntilDebuggerAttaches: boolean; +}; + +export async function getDebugpyLauncherArgs(options: RemoteDebugOptions, debuggerPath?: string) { + if (!debuggerPath) { + debuggerPath = await getDebugpyPath(); + } + + const waitArgs = options.waitUntilDebuggerAttaches ? ['--wait-for-client'] : []; + return [ + debuggerPath.fileToCommandArgumentForPythonExt(), + '--listen', + `${options.host}:${options.port}`, + ...waitArgs, + ]; +} diff --git a/src/client/debugger/extension/adapter/types.ts b/src/client/debugger/extension/adapter/types.ts new file mode 100644 index 000000000000..6c082a801ad6 --- /dev/null +++ b/src/client/debugger/extension/adapter/types.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export const IPromptShowState = Symbol('IPromptShowState'); +export interface IPromptShowState { + shouldShowPrompt(): boolean; + setShowPrompt(show: boolean): void; +} diff --git a/src/client/debugger/extension/attachQuickPick/factory.ts b/src/client/debugger/extension/attachQuickPick/factory.ts new file mode 100644 index 000000000000..627962106e88 --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/factory.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { IApplicationShell, ICommandManager } from '../../../common/application/types'; +import { Commands } from '../../../common/constants'; +import { IPlatformService } from '../../../common/platform/types'; +import { IProcessServiceFactory } from '../../../common/process/types'; +import { IDisposableRegistry } from '../../../common/types'; +import { AttachPicker } from './picker'; +import { AttachProcessProvider } from './provider'; +import { IAttachProcessProviderFactory } from './types'; + +@injectable() +export class AttachProcessProviderFactory implements IAttachProcessProviderFactory { + constructor( + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + ) {} + + public registerCommands() { + const provider = new AttachProcessProvider(this.platformService, this.processServiceFactory); + const picker = new AttachPicker(this.applicationShell, provider); + const disposable = this.commandManager.registerCommand( + Commands.PickLocalProcess, + () => picker.showQuickPick(), + this, + ); + this.disposableRegistry.push(disposable); + } +} diff --git a/src/client/debugger/extension/attachQuickPick/picker.ts b/src/client/debugger/extension/attachQuickPick/picker.ts new file mode 100644 index 000000000000..a296a9b3163a --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/picker.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Disposable } from 'vscode'; +import { IApplicationShell } from '../../../common/application/types'; +import { getIcon } from '../../../common/utils/icons'; +import { AttachProcess } from '../../../common/utils/localize'; +import { IAttachItem, IAttachPicker, IAttachProcessProvider, REFRESH_BUTTON_ICON } from './types'; + +@injectable() +export class AttachPicker implements IAttachPicker { + constructor( + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + private readonly attachItemsProvider: IAttachProcessProvider, + ) {} + + public showQuickPick(): Promise<string> { + return new Promise<string>(async (resolve, reject) => { + const processEntries = await this.attachItemsProvider.getAttachItems(); + + const refreshButton = { + iconPath: getIcon(REFRESH_BUTTON_ICON), + tooltip: AttachProcess.refreshList, + }; + + const quickPick = this.applicationShell.createQuickPick<IAttachItem>(); + quickPick.title = AttachProcess.attachTitle; + quickPick.placeholder = AttachProcess.selectProcessPlaceholder; + quickPick.canSelectMany = false; + quickPick.matchOnDescription = true; + quickPick.matchOnDetail = true; + quickPick.items = processEntries; + quickPick.buttons = [refreshButton]; + + const disposables: Disposable[] = []; + + quickPick.onDidTriggerButton( + async () => { + quickPick.busy = true; + const attachItems = await this.attachItemsProvider.getAttachItems(); + quickPick.items = attachItems; + quickPick.busy = false; + }, + this, + disposables, + ); + + quickPick.onDidAccept( + () => { + if (quickPick.selectedItems.length !== 1) { + reject(new Error(AttachProcess.noProcessSelected)); + } + + const selectedId = quickPick.selectedItems[0].id; + + disposables.forEach((item) => item.dispose()); + quickPick.dispose(); + + resolve(selectedId); + }, + undefined, + disposables, + ); + + quickPick.onDidHide( + () => { + disposables.forEach((item) => item.dispose()); + quickPick.dispose(); + + reject(new Error(AttachProcess.noProcessSelected)); + }, + undefined, + disposables, + ); + + quickPick.show(); + }); + } +} diff --git a/src/client/debugger/extension/attachQuickPick/provider.ts b/src/client/debugger/extension/attachQuickPick/provider.ts new file mode 100644 index 000000000000..3626d8dfb8ce --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/provider.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { l10n } from 'vscode'; +import { IPlatformService } from '../../../common/platform/types'; +import { IProcessServiceFactory } from '../../../common/process/types'; +import { PsProcessParser } from './psProcessParser'; +import { IAttachItem, IAttachProcessProvider, ProcessListCommand } from './types'; +import { WmicProcessParser } from './wmicProcessParser'; + +@injectable() +export class AttachProcessProvider implements IAttachProcessProvider { + constructor( + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, + ) {} + + public getAttachItems(): Promise<IAttachItem[]> { + return this._getInternalProcessEntries().then((processEntries) => { + processEntries.sort( + ( + { processName: aprocessName, commandLine: aCommandLine }, + { processName: bProcessName, commandLine: bCommandLine }, + ) => { + const compare = (aString: string, bString: string): number => { + // localeCompare is significantly slower than < and > (2000 ms vs 80 ms for 10,000 elements) + // We can change to localeCompare if this becomes an issue + const aLower = aString.toLowerCase(); + const bLower = bString.toLowerCase(); + + if (aLower === bLower) { + return 0; + } + + return aLower < bLower ? -1 : 1; + }; + + const aPython = aprocessName.startsWith('python'); + const bPython = bProcessName.startsWith('python'); + + if (aPython || bPython) { + if (aPython && !bPython) { + return -1; + } + if (bPython && !aPython) { + return 1; + } + + return aPython ? compare(aCommandLine!, bCommandLine!) : compare(bCommandLine!, aCommandLine!); + } + + return compare(aprocessName, bProcessName); + }, + ); + + return processEntries; + }); + } + + public async _getInternalProcessEntries(): Promise<IAttachItem[]> { + let processCmd: ProcessListCommand; + if (this.platformService.isMac) { + processCmd = PsProcessParser.psDarwinCommand; + } else if (this.platformService.isLinux) { + processCmd = PsProcessParser.psLinuxCommand; + } else if (this.platformService.isWindows) { + processCmd = WmicProcessParser.wmicCommand; + } else { + throw new Error(l10n.t("Operating system '{0}' not supported.", this.platformService.osType)); + } + + const processService = await this.processServiceFactory.create(); + const output = await processService.exec(processCmd.command, processCmd.args, { throwOnStdErr: true }); + + return this.platformService.isWindows + ? WmicProcessParser.parseProcesses(output.stdout) + : PsProcessParser.parseProcesses(output.stdout); + } +} diff --git a/src/client/debugger/extension/attachQuickPick/psProcessParser.ts b/src/client/debugger/extension/attachQuickPick/psProcessParser.ts new file mode 100644 index 000000000000..843369bd00c7 --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/psProcessParser.ts @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IAttachItem, ProcessListCommand } from './types'; + +export namespace PsProcessParser { + const secondColumnCharacters = 50; + const commColumnTitle = ''.padStart(secondColumnCharacters, 'a'); + + // Perf numbers: + // OS X 10.10 + // | # of processes | Time (ms) | + // |----------------+-----------| + // | 272 | 52 | + // | 296 | 49 | + // | 384 | 53 | + // | 784 | 116 | + // + // Ubuntu 16.04 + // | # of processes | Time (ms) | + // |----------------+-----------| + // | 232 | 26 | + // | 336 | 34 | + // | 736 | 62 | + // | 1039 | 115 | + // | 1239 | 182 | + + // ps outputs as a table. With the option "ww", ps will use as much width as necessary. + // However, that only applies to the right-most column. Here we use a hack of setting + // the column header to 50 a's so that the second column will have at least that many + // characters. 50 was chosen because that's the maximum length of a "label" in the + // QuickPick UI in VS Code. + + // the BSD version of ps uses '-c' to have 'comm' only output the executable name and not + // the full path. The Linux version of ps has 'comm' to only display the name of the executable + // Note that comm on Linux systems is truncated to 16 characters: + // https://bugzilla.redhat.com/show_bug.cgi?id=429565 + // Since 'args' contains the full path to the executable, even if truncated, searching will work as desired. + export const psLinuxCommand: ProcessListCommand = { + command: 'ps', + args: ['axww', '-o', `pid=,comm=${commColumnTitle},args=`], + }; + export const psDarwinCommand: ProcessListCommand = { + command: 'ps', + args: ['axww', '-o', `pid=,comm=${commColumnTitle},args=`, '-c'], + }; + + export function parseProcesses(processes: string): IAttachItem[] { + const lines: string[] = processes.split('\n'); + return parseProcessesFromPsArray(lines); + } + + function parseProcessesFromPsArray(processArray: string[]): IAttachItem[] { + const processEntries: IAttachItem[] = []; + + // lines[0] is the header of the table + for (let i = 1; i < processArray.length; i += 1) { + const line = processArray[i]; + if (!line) { + continue; + } + + const processEntry = parseLineFromPs(line); + if (processEntry) { + processEntries.push(processEntry); + } + } + + return processEntries; + } + + function parseLineFromPs(line: string): IAttachItem | undefined { + // Explanation of the regex: + // - any leading whitespace + // - PID + // - whitespace + // - executable name --> this is PsAttachItemsProvider.secondColumnCharacters - 1 because ps reserves one character + // for the whitespace separator + // - whitespace + // - args (might be empty) + const psEntry: RegExp = new RegExp(`^\\s*([0-9]+)\\s+(.{${secondColumnCharacters - 1}})\\s+(.*)$`); + const matches = psEntry.exec(line); + + if (matches?.length === 4) { + const pid = matches[1].trim(); + const executable = matches[2].trim(); + const cmdline = matches[3].trim(); + + return { + label: executable, + description: pid, + detail: cmdline, + id: pid, + processName: executable, + commandLine: cmdline, + }; + } + } +} diff --git a/src/client/debugger/extension/attachQuickPick/types.ts b/src/client/debugger/extension/attachQuickPick/types.ts new file mode 100644 index 000000000000..5e26c1354f9e --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/types.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { QuickPickItem } from 'vscode'; + +export type ProcessListCommand = { command: string; args: string[] }; + +export interface IAttachItem extends QuickPickItem { + id: string; + processName: string; + commandLine: string; +} + +export interface IAttachProcessProvider { + getAttachItems(): Promise<IAttachItem[]>; +} + +export const IAttachProcessProviderFactory = Symbol('IAttachProcessProviderFactory'); +export interface IAttachProcessProviderFactory { + registerCommands(): void; +} + +export interface IAttachPicker { + showQuickPick(): Promise<string>; +} + +export const REFRESH_BUTTON_ICON = 'refresh.svg'; diff --git a/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts b/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts new file mode 100644 index 000000000000..e1faed50fc2e --- /dev/null +++ b/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IAttachItem, ProcessListCommand } from './types'; + +export namespace WmicProcessParser { + const wmicNameTitle = 'Name'; + const wmicCommandLineTitle = 'CommandLine'; + const wmicPidTitle = 'ProcessId'; + const defaultEmptyEntry: IAttachItem = { + label: '', + description: '', + detail: '', + id: '', + processName: '', + commandLine: '', + }; + + // Perf numbers on Win10: + // | # of processes | Time (ms) | + // |----------------+-----------| + // | 309 | 413 | + // | 407 | 463 | + // | 887 | 746 | + // | 1308 | 1132 | + export const wmicCommand: ProcessListCommand = { + command: 'wmic', + args: ['process', 'get', 'Name,ProcessId,CommandLine', '/FORMAT:list'], + }; + + export function parseProcesses(processes: string): IAttachItem[] { + const lines: string[] = processes.split('\r\n'); + const processEntries: IAttachItem[] = []; + let entry = { ...defaultEmptyEntry }; + + for (const line of lines) { + if (!line.length) { + continue; + } + + parseLineFromWmic(line, entry); + + // Each entry of processes has ProcessId as the last line + if (line.lastIndexOf(wmicPidTitle, 0) === 0) { + processEntries.push(entry); + entry = { ...defaultEmptyEntry }; + } + } + + return processEntries; + } + + function parseLineFromWmic(line: string, item: IAttachItem): IAttachItem { + const splitter = line.indexOf('='); + const currentItem = item; + + if (splitter > 0) { + const key = line.slice(0, splitter).trim(); + let value = line.slice(splitter + 1).trim(); + + if (key === wmicNameTitle) { + currentItem.label = value; + currentItem.processName = value; + } else if (key === wmicPidTitle) { + currentItem.description = value; + currentItem.id = value; + } else if (key === wmicCommandLineTitle) { + const dosDevicePrefix = '\\??\\'; // DOS device prefix, see https://reverseengineering.stackexchange.com/a/15178 + if (value.lastIndexOf(dosDevicePrefix, 0) === 0) { + value = value.slice(dosDevicePrefix.length); + } + + currentItem.detail = value; + currentItem.commandLine = value; + } + } + + return currentItem; + } +} diff --git a/src/client/debugger/extension/banner.ts b/src/client/debugger/extension/banner.ts deleted file mode 100644 index 42315a144cad..000000000000 --- a/src/client/debugger/extension/banner.ts +++ /dev/null @@ -1,191 +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 { IBrowserService, IDisposableRegistry, - ILogger, 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>(IPersistentStateFactory); - const key = PersistentStateKeys.ShowBanner; - const state = factory.createGlobalPersistentState<boolean>(key, true); - return state.value; - } - - public async disable(): Promise<void> { - const factory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory); - const key = PersistentStateKeys.ShowBanner; - const state = factory.createGlobalPersistentState<boolean>(key, false); - await state.updateValue(false); - } - - // showing banner - - public async shouldShow(): Promise<boolean> { - if (!this.isEnabled() || this.disabledInCurrentSession) { - return false; - } - if (! await this.passedThreshold()) { - return false; - } - return this.isUserSelected(); - } - - public async show(): Promise<void> { - const appShell = this.serviceContainer.get<IApplicationShell>(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<void> { - const debuggerLaunchCounter = await this.getGetDebuggerLaunchCounter(); - const browser = this.serviceContainer.get<IBrowserService>(IBrowserService); - browser.launch(`https://www.research.net/r/N7B25RV?n=${debuggerLaunchCounter}`); - } - - // user selection - - private async isUserSelected(): Promise<boolean> { - if (this.userSelected !== undefined) { - return this.userSelected; - } - - const factory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory); - const key = PersistentStateKeys.UserSelected; - const state = factory.createGlobalPersistentState<boolean|undefined>(key, undefined); - let selected = state.value; - if (selected === undefined) { - const runtime = this.serviceContainer.get<IRandom>(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<boolean> { - const [threshold, debuggerCounter] = await Promise.all([ - this.getDebuggerLaunchThresholdCounter(), - this.getGetDebuggerLaunchCounter() - ]); - return debuggerCounter >= threshold; - } - - private async incrementDebuggerLaunchCounter(): Promise<void> { - const factory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory); - const key = PersistentStateKeys.DebuggerLaunchCounter; - const state = factory.createGlobalPersistentState<number>(key, 0); - await state.updateValue(state.value + 1); - } - - private async getGetDebuggerLaunchCounter(): Promise<number> { - const factory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory); - const key = PersistentStateKeys.DebuggerLaunchCounter; - const state = factory.createGlobalPersistentState<number>(key, 0); - return state.value; - } - - private async getDebuggerLaunchThresholdCounter(): Promise<number> { - const factory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory); - const key = PersistentStateKeys.DebuggerLaunchThresholdCounter; - const state = factory.createGlobalPersistentState<number | undefined>(key, undefined); - if (state.value === undefined) { - const runtime = this.serviceContainer.get<IRandom>(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>(IDebugService); - const disposable = debuggerService.onDidTerminateDebugSession(async e => { - if (e.type === DebuggerTypeName) { - const logger = this.serviceContainer.get<ILogger>(ILogger); - await this.onDidTerminateDebugSession() - .catch(ex => logger.logError('Error in debugger Banner', ex)); - } - }); - this.serviceContainer.get<Disposable[]>(IDisposableRegistry).push(disposable); - } - - private async onDidTerminateDebugSession(): Promise<void> { - 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/configurationProviderUtils.ts b/src/client/debugger/extension/configuration/configurationProviderUtils.ts deleted file mode 100644 index 8a8c56103e3d..000000000000 --- a/src/client/debugger/extension/configuration/configurationProviderUtils.ts +++ /dev/null @@ -1,37 +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 { IApplicationShell } from '../../../common/application/types'; -import { traceError } from '../../../common/logger'; -import { IFileSystem } from '../../../common/platform/types'; -import { IPythonExecutionFactory } from '../../../common/process/types'; -import { noop } from '../../../common/utils/misc'; -import { IConfigurationProviderUtils } from './types'; - -const PSERVE_SCRIPT_FILE_NAME = 'pserve.py'; - -@injectable() -export class ConfigurationProviderUtils implements IConfigurationProviderUtils { - constructor(@inject(IPythonExecutionFactory) private readonly executionFactory: IPythonExecutionFactory, - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IApplicationShell) private readonly shell: IApplicationShell) { - } - public async getPyramidStartupScriptFilePath(resource?: Uri): Promise<string | undefined> { - try { - const executionService = await this.executionFactory.create({ resource }); - const output = await executionService.exec(['-c', 'import pyramid;print(pyramid.__file__)'], { throwOnStdErr: true }); - const pserveFilePath = path.join(path.dirname(output.stdout.trim()), 'scripts', PSERVE_SCRIPT_FILE_NAME); - return await this.fs.fileExists(pserveFilePath) ? pserveFilePath : undefined; - } catch (ex) { - const message = 'Unable to locate \'pserve.py\' required for debugging of Pyramid applications.'; - traceError(message, ex); - this.shell.showErrorMessage(message).then(noop, noop); - return; - } - } -} diff --git a/src/client/debugger/extension/configuration/debugConfigurationService.ts b/src/client/debugger/extension/configuration/debugConfigurationService.ts index d45bf0478982..9997fb4f0509 100644 --- a/src/client/debugger/extension/configuration/debugConfigurationService.ts +++ b/src/client/debugger/extension/configuration/debugConfigurationService.ts @@ -4,70 +4,60 @@ 'use strict'; import { inject, injectable, named } from 'inversify'; -import * as path from 'path'; -import { CancellationToken, DebugConfiguration, QuickPickItem, WorkspaceFolder } from 'vscode'; -import { IFileSystem } from '../../../common/platform/types'; -import { DebugConfigurationPrompts } from '../../../common/utils/localize'; -import { IMultiStepInput, IMultiStepInputFactory, InputStep, IQuickPickParameters } from '../../../common/utils/multiStepInput'; -import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { sendTelemetryEvent } from '../../../telemetry'; -import { DEBUGGER_CONFIGURATION_PROMPTS } from '../../../telemetry/constants'; -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 { - constructor(@inject(IDebugConfigurationResolver) @named('attach') private readonly attachResolver: IDebugConfigurationResolver<AttachRequestArguments>, - @inject(IDebugConfigurationResolver) @named('launch') private readonly launchResolver: IDebugConfigurationResolver<LaunchRequestArguments>, - @inject(IDebugConfigurationProviderFactory) private readonly providerFactory: IDebugConfigurationProviderFactory, - @inject(IMultiStepInputFactory) private readonly multiStepFactory: IMultiStepInputFactory, - @inject(IFileSystem) private readonly fs: IFileSystem) { - } - public async provideDebugConfigurations(folder: WorkspaceFolder | undefined, token?: CancellationToken): Promise<DebugConfiguration[] | undefined> { - const config: Partial<DebugConfigurationArguments> = {}; - const state = { config, folder, token }; - const multiStep = this.multiStepFactory.create<DebugConfigurationState>(); - await multiStep.run((input, s) => this.pickDebugConfiguration(input, s), state); - if (Object.keys(state.config).length === 0) { - return this.getDefaultDebugConfig(); - } else { - return [state.config as DebugConfiguration]; - } - } - public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise<DebugConfiguration | undefined> { + constructor( + @inject(IDebugConfigurationResolver) + @named('attach') + private readonly attachResolver: IDebugConfigurationResolver<AttachRequestArguments>, + @inject(IDebugConfigurationResolver) + @named('launch') + private readonly launchResolver: IDebugConfigurationResolver<LaunchRequestArguments>, + ) {} + + public async resolveDebugConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: DebugConfiguration, + token?: CancellationToken, + ): Promise<DebugConfiguration | undefined> { if (debugConfiguration.request === 'attach') { - return this.attachResolver.resolveDebugConfiguration(folder, debugConfiguration as AttachRequestArguments, token); + return this.attachResolver.resolveDebugConfiguration( + folder, + debugConfiguration as AttachRequestArguments, + 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 { - return this.launchResolver.resolveDebugConfiguration(folder, debugConfiguration as LaunchRequestArguments, token); + if (Object.keys(debugConfiguration).length === 0) { + return undefined; + } + return this.launchResolver.resolveDebugConfiguration( + folder, + debugConfiguration as LaunchRequestArguments, + token, + ); } } - protected async getDefaultDebugConfig(): Promise<DebugConfiguration[]> { - sendTelemetryEvent(DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.default }); - const jsFilePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'default.launch.json'); - const jsonStr = await this.fs.readFile(jsFilePath); - return JSON.parse(jsonStr) as DebugConfiguration[]; - } - protected async pickDebugConfiguration(input: IMultiStepInput<DebugConfigurationState>, state: DebugConfigurationState): Promise<InputStep<DebugConfigurationState> | void> { - type DebugConfigurationQuickPickItem = QuickPickItem & { type: DebugConfigurationType }; - const items: DebugConfigurationQuickPickItem[] = [ - { label: DebugConfigurationPrompts.debugFileConfigurationLabel(), type: DebugConfigurationType.launchFile, description: DebugConfigurationPrompts.debugFileConfigurationDescription() }, - { label: DebugConfigurationPrompts.debugModuleConfigurationLabel(), type: DebugConfigurationType.launchModule, description: DebugConfigurationPrompts.debugModuleConfigurationDescription() }, - { label: DebugConfigurationPrompts.remoteAttachConfigurationLabel(), type: DebugConfigurationType.remoteAttach, description: DebugConfigurationPrompts.remoteAttachConfigurationDescription() }, - { label: DebugConfigurationPrompts.debugDjangoConfigurationLabel(), type: DebugConfigurationType.launchDjango, description: DebugConfigurationPrompts.debugDjangoConfigurationDescription() }, - { label: DebugConfigurationPrompts.debugFlaskConfigurationLabel(), type: DebugConfigurationType.launchFlask, description: DebugConfigurationPrompts.debugFlaskConfigurationDescription() }, - { label: DebugConfigurationPrompts.debugPyramidConfigurationLabel(), type: DebugConfigurationType.launchPyramid, description: DebugConfigurationPrompts.debugPyramidConfigurationDescription() } - ]; - state.config = {}; - const pick = await input.showQuickPick<DebugConfigurationQuickPickItem, IQuickPickParameters<DebugConfigurationQuickPickItem>>({ - title: DebugConfigurationPrompts.selectConfigurationTitle(), - placeholder: DebugConfigurationPrompts.selectConfigurationPlaceholder(), - 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<DebugConfiguration | undefined> { + function resolve<T extends DebugConfiguration>(resolver: IDebugConfigurationResolver<T>) { + 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/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<DebugConfiguration[]> { + 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<DebugConfiguration[]> { + if (uri) { + const workspace = getWorkspaceFolder(uri); + if (workspace) { + return getConfigurationsForWorkspace(workspace); + } + } + return []; +} 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 2cfc9aa927d7..000000000000 --- a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts +++ /dev/null @@ -1,90 +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 { DebugConfigurationPrompts, localize } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { SystemVariables } from '../../../../common/variables/systemVariables'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { DEBUGGER_CONFIGURATION_PROMPTS } 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<DebugConfigurationState>, state: DebugConfigurationState) { - const program = await this.getManagePyPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const defaultProgram = `${workspaceFolderToken}${this.pathUtils.separator}manage.py`; - const config: Partial<LaunchRequestArguments> = { - name: localize('python.snippet.launch.django.label', 'Python: Django')(), - type: DebuggerTypeName, - request: 'launch', - program: program || defaultProgram, - args: [ - 'runserver', - '--noreload', - '--nothreading' - ], - django: true - }; - if (!program) { - const selectedProgram = await input.showInputBox({ - title: DebugConfigurationPrompts.djangoEnterManagePyPathTitle(), - value: defaultProgram, - prompt: DebugConfigurationPrompts.djangoEnterManagePyPathPrompt(), - validate: value => this.validateManagePy(state.folder, defaultProgram, value) - }); - if (selectedProgram) { - manuallyEnteredAValue = true; - config.program = selectedProgram; - } - } - - sendTelemetryEvent(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<string | undefined> { - const error = DebugConfigurationPrompts.djangoEnterManagePyPathInvalidFilePathError(); - 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 workspaceFolder = resource ? this.workspace.getWorkspaceFolder(resource) : undefined; - const systemVariables = new SystemVariables(workspaceFolder ? workspaceFolder.uri.fsPath : undefined); - return systemVariables.resolveAny(pythonPath); - } - - protected async getManagePyPath(folder: WorkspaceFolder | undefined): Promise<string | undefined> { - 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 7718778ff24e..000000000000 --- a/src/client/debugger/extension/configuration/providers/fileLaunch.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 { localize } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { captureTelemetry } from '../../../../telemetry'; -import { DEBUGGER_CONFIGURATION_PROMPTS } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; - -@injectable() -export class FileLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - @captureTelemetry(DEBUGGER_CONFIGURATION_PROMPTS, { configurationType: DebugConfigurationType.launchFile }, false) - public async buildConfiguration(_input: MultiStepInput<DebugConfigurationState>, state: DebugConfigurationState) { - const config: Partial<LaunchRequestArguments> = { - name: localize('python.snippet.launch.standard.label', 'Python: Current File')(), - 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 1f21c10feff4..000000000000 --- a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts +++ /dev/null @@ -1,70 +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 { DebugConfigurationPrompts, localize } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { DEBUGGER_CONFIGURATION_PROMPTS } 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<DebugConfigurationState>, state: DebugConfigurationState) { - const application = await this.getApplicationPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const config: Partial<LaunchRequestArguments> = { - name: localize('python.snippet.launch.flask.label', 'Python: Flask')(), - 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: DebugConfigurationPrompts.flaskEnterAppPathOrNamePathTitle(), - value: 'app.py', - prompt: DebugConfigurationPrompts.debugFlaskConfigurationDescription(), - validate: value => Promise.resolve((value && value.trim().length > 0) ? undefined : DebugConfigurationPrompts.flaskEnterAppPathOrNamePathInvalidNameError()) - }); - if (selectedApp) { - manuallyEnteredAValue = true; - config.env!.FLASK_APP = selectedApp; - } - } - - sendTelemetryEvent(DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.launchFlask, autoDetectedFlaskAppPyPath: !!application, manuallyEnteredAValue }); - Object.assign(state.config, config); - } - protected async getApplicationPath(folder: WorkspaceFolder | undefined): Promise<string | undefined> { - 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 baf11fa32f04..000000000000 --- a/src/client/debugger/extension/configuration/providers/moduleLaunch.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 { DebugConfigurationPrompts, localize } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { DEBUGGER_CONFIGURATION_PROMPTS } 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<DebugConfigurationState>, state: DebugConfigurationState) { - let manuallyEnteredAValue: boolean | undefined; - const config: Partial<LaunchRequestArguments> = { - name: localize('python.snippet.launch.module.label', 'Python: Module')(), - type: DebuggerTypeName, - request: 'launch', - module: 'enter-your-module-name' - }; - const selectedModule = await input.showInputBox({ - title: DebugConfigurationPrompts.moduleEnterModuleTitle(), - value: config.module || 'enter-your-module-name', - prompt: DebugConfigurationPrompts.moduleEnterModulePrompt(), - validate: value => Promise.resolve((value && value.trim().length > 0) ? undefined : DebugConfigurationPrompts.moduleEnterModuleInvalidNameError()) - }); - if (selectedModule) { - manuallyEnteredAValue = true; - config.module = selectedModule; - } - - sendTelemetryEvent(DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.launchModule, manuallyEnteredAValue }); - 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 61f808d1e9e1..000000000000 --- a/src/client/debugger/extension/configuration/providers/providerFactory.ts +++ /dev/null @@ -1,32 +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<DebugConfigurationType, IDebugConfigurationProvider>; - 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 - ) { - this.providers = new Map<DebugConfigurationType, IDebugConfigurationProvider>(); - 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); - } - 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 da50c6703ed2..000000000000 --- a/src/client/debugger/extension/configuration/providers/pyramidLaunch.ts +++ /dev/null @@ -1,92 +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 { DebugConfigurationPrompts, localize } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { SystemVariables } from '../../../../common/variables/systemVariables'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { DEBUGGER_CONFIGURATION_PROMPTS } 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<DebugConfigurationState>, state: DebugConfigurationState) { - const iniPath = await this.getDevelopmentIniPath(state.folder); - const defaultIni = `${workspaceFolderToken}${this.pathUtils.separator}development.ini`; - let manuallyEnteredAValue: boolean | undefined; - - const config: Partial<LaunchRequestArguments> = { - name: localize('python.snippet.launch.pyramid.label', 'Python: Pyramid Application')(), - type: DebuggerTypeName, - request: 'launch', - args: [ - iniPath || defaultIni - ], - pyramid: true, - jinja: true - }; - - if (!iniPath) { - const selectedIniPath = await input.showInputBox({ - title: DebugConfigurationPrompts.pyramidEnterDevelopmentIniPathTitle(), - value: defaultIni, - prompt: DebugConfigurationPrompts.pyramidEnterDevelopmentIniPathPrompt(), - validate: value => this.validateIniPath(state ? state.folder : undefined, defaultIni, value) - }); - if (selectedIniPath) { - manuallyEnteredAValue = true; - config.args = [selectedIniPath]; - } - } - - sendTelemetryEvent(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<string | undefined> { - if (!folder) { - return; - } - const error = DebugConfigurationPrompts.pyramidEnterDevelopmentIniPathInvalidFilePathError(); - 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 workspaceFolder = resource ? this.workspace.getWorkspaceFolder(resource) : undefined; - const systemVariables = new SystemVariables(workspaceFolder ? workspaceFolder.uri.fsPath : undefined); - return systemVariables.resolveAny(pythonPath); - } - - protected async getDevelopmentIniPath(folder: WorkspaceFolder | undefined): Promise<string | undefined> { - 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 9d6fee8c307e..000000000000 --- a/src/client/debugger/extension/configuration/providers/remoteAttach.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { injectable } from 'inversify'; -import { DebugConfigurationPrompts, localize } from '../../../../common/utils/localize'; -import { InputStep, MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { DEBUGGER_CONFIGURATION_PROMPTS } 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<DebugConfigurationState>, state: DebugConfigurationState): Promise<InputStep<DebugConfigurationState> | void> { - const config: Partial<AttachRequestArguments> = { - name: localize('python.snippet.launch.attach.label', 'Python: Attach')(), - type: DebuggerTypeName, - request: 'attach', - port: defaultPort, - host: defaultHost - }; - - config.host = await input.showInputBox({ - title: DebugConfigurationPrompts.attachRemoteHostTitle(), - step: 1, - totalSteps: 2, - value: config.host || defaultHost, - prompt: DebugConfigurationPrompts.attachRemoteHostPrompt(), - validate: value => Promise.resolve((value && value.trim().length > 0) ? undefined : DebugConfigurationPrompts.attachRemoteHostValidationError()) - }); - if (!config.host) { - config.host = defaultHost; - } - - sendTelemetryEvent(DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.remoteAttach, manuallyEnteredAValue: config.host !== defaultHost }); - Object.assign(state.config, config); - return _ => this.configurePort(input, state.config); - } - protected async configurePort(input: MultiStepInput<DebugConfigurationState>, config: Partial<AttachRequestArguments>) { - const port = await input.showInputBox({ - title: DebugConfigurationPrompts.attachRemotePortTitle(), - step: 2, - totalSteps: 2, - value: (config.port || defaultPort).toString(), - prompt: DebugConfigurationPrompts.attachRemotePortPrompt(), - validate: value => Promise.resolve((value && /^\d+$/.test(value.trim())) ? undefined : DebugConfigurationPrompts.attachRemotePortValidationError()) - }); - if (port && /^\d+$/.test(port.trim())) { - config.port = parseInt(port, 10); - } - if (!config.port) { - config.port = defaultPort; - } - sendTelemetryEvent(DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.remoteAttach, manuallyEnteredAValue: config.port !== defaultPort }); - } -} diff --git a/src/client/debugger/extension/configuration/resolvers/attach.ts b/src/client/debugger/extension/configuration/resolvers/attach.ts index 70e732ec27d8..1c232f261d03 100644 --- a/src/client/debugger/extension/configuration/resolvers/attach.ts +++ b/src/client/debugger/extension/configuration/resolvers/attach.ts @@ -3,96 +3,114 @@ '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 { AttachRequestArguments, DebugOptions } from '../../../types'; +import { getOSType, OSType } from '../../../../common/utils/platform'; +import { AttachRequestArguments, DebugOptions, PathMapping } from '../../../types'; import { BaseConfigurationResolver } from './base'; @injectable() export class AttachConfigurationResolver extends BaseConfigurationResolver<AttachRequestArguments> { - constructor(@inject(IWorkspaceService) workspaceService: IWorkspaceService, - @inject(IDocumentManager) documentManager: IDocumentManager, - @inject(IPlatformService) private readonly platformService: IPlatformService, - @inject(IConfigurationService) configurationService: IConfigurationService) { - super(workspaceService, documentManager, configurationService); - } - public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: AttachRequestArguments, token?: CancellationToken): Promise<AttachRequestArguments | undefined> { - const workspaceFolder = this.getWorkspaceFolder(folder); + public async resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: AttachRequestArguments, + _token?: CancellationToken, + ): Promise<AttachRequestArguments | undefined> { + 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); + dbgConfig.debugOptions = dbgConfig.debugOptions!.filter( + (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): Promise<void> { + + protected async provideAttachDefaults( + workspaceFolder: Uri | undefined, + debugConfiguration: AttachRequestArguments, + ): Promise<void> { if (!Array.isArray(debugConfiguration.debugOptions)) { debugConfiguration.debugOptions = []; } - if (!debugConfiguration.host) { + if (!(debugConfiguration.connect || debugConfiguration.listen) && !debugConfiguration.host) { + // Connect and listen cannot be mixed with host property. debugConfiguration.host = 'localhost'; } + debugConfiguration.showReturnValue = debugConfiguration.showReturnValue !== false; // Pass workspace folder so we can get this when we get debug events firing. debugConfiguration.workspaceFolder = workspaceFolder ? workspaceFolder.fsPath : undefined; const debugOptions = debugConfiguration.debugOptions!; - if (debugConfiguration.debugStdLib) { - this.debugOption(debugOptions, DebugOptions.DebugStdLib); - } if (debugConfiguration.django) { - this.debugOption(debugOptions, DebugOptions.Django); + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Django); } if (debugConfiguration.jinja) { - this.debugOption(debugOptions, DebugOptions.Jinja); + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Jinja); } if (debugConfiguration.subProcess === true) { - this.debugOption(debugOptions, DebugOptions.SubProcess); + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.SubProcess); } - if (debugConfiguration.pyramid - && debugOptions.indexOf(DebugOptions.Jinja) === -1 - && debugConfiguration.jinja !== false) { - this.debugOption(debugOptions, DebugOptions.Jinja); + if ( + debugConfiguration.pyramid && + debugOptions.indexOf(DebugOptions.Jinja) === -1 && + debugConfiguration.jinja !== false + ) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.Jinja); } if (debugConfiguration.redirectOutput || debugConfiguration.redirectOutput === undefined) { - this.debugOption(debugOptions, DebugOptions.RedirectOutput); + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.RedirectOutput); } // We'll need paths to be fixed only in the case where local and remote hosts are the same // I.e. only if hostName === 'localhost' or '127.0.0.1' or '' - const isLocalHost = this.isLocalHost(debugConfiguration.host); - if (this.platformService.isWindows && isLocalHost) { - this.debugOption(debugOptions, DebugOptions.FixFilePathCase); + const isLocalHost = AttachConfigurationResolver.isLocalHost(debugConfiguration.host); + if (getOSType() === OSType.Windows && isLocalHost) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.FixFilePathCase); } - if (this.platformService.isWindows) { - this.debugOption(debugOptions, DebugOptions.WindowsClient); - } else { - this.debugOption(debugOptions, DebugOptions.UnixClient); + if (debugConfiguration.clientOS === undefined) { + debugConfiguration.clientOS = getOSType() === OSType.Windows ? 'windows' : 'unix'; } - - if (!debugConfiguration.pathMappings) { - debugConfiguration.pathMappings = []; + if (debugConfiguration.showReturnValue) { + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.ShowReturnValue); } + + debugConfiguration.pathMappings = this.resolvePathMappings( + debugConfiguration.pathMappings || [], + debugConfiguration.host, + debugConfiguration.localRoot, + debugConfiguration.remoteRoot, + workspaceFolder, + ); + } + + // eslint-disable-next-line class-methods-use-this + private resolvePathMappings( + pathMappings: PathMapping[], + host?: string, + localRoot?: string, + remoteRoot?: string, + workspaceFolder?: Uri, + ) { // This is for backwards compatibility. - if (debugConfiguration.localRoot && debugConfiguration.remoteRoot) { - debugConfiguration.pathMappings!.push({ - localRoot: debugConfiguration.localRoot, - remoteRoot: debugConfiguration.remoteRoot + if (localRoot && remoteRoot) { + pathMappings.push({ + localRoot, + remoteRoot, }); } // If attaching to local host, then always map local root and remote roots. - if (workspaceFolder && debugConfiguration.host && - debugConfiguration.pathMappings!.length === 0 && - ['LOCALHOST', '127.0.0.1', '::1'].indexOf(debugConfiguration.host.toUpperCase()) >= 0) { - debugConfiguration.pathMappings!.push({ - localRoot: workspaceFolder.fsPath, - remoteRoot: workspaceFolder.fsPath - }); + if (AttachConfigurationResolver.isLocalHost(host)) { + pathMappings = AttachConfigurationResolver.fixUpPathMappings( + pathMappings, + workspaceFolder ? workspaceFolder.fsPath : '', + ); } - this.sendTelemetry('attach', debugConfiguration); + return pathMappings.length > 0 ? pathMappings : undefined; } } diff --git a/src/client/debugger/extension/configuration/resolvers/base.ts b/src/client/debugger/extension/configuration/resolvers/base.ts index dffff2e0c6f5..fde55ad8d5ea 100644 --- a/src/client/debugger/extension/configuration/resolvers/base.ts +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -3,96 +3,226 @@ 'use strict'; -// tslint:disable:no-invalid-template-strings - 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 { IConfigurationService } from '../../../../common/types'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { DEBUGGER } from '../../../../telemetry/constants'; -import { DebuggerTelemetry } from '../../../../telemetry/types'; -import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../../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<T extends DebugConfiguration> implements IDebugConfigurationResolver<T> { - constructor(protected readonly workspaceService: IWorkspaceService, - protected readonly documentManager: IDocumentManager, - protected readonly configurationService: IConfigurationService) { } - public abstract resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise<T | undefined>; - protected getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { +export abstract class BaseConfigurationResolver<T extends DebugConfiguration> + implements IDebugConfigurationResolver<T> { + protected pythonPathSource: PythonPathSource = PythonPathSource.launchJson; + + constructor( + protected readonly configurationService: IConfigurationService, + protected readonly interpreterService: IInterpreterService, + ) {} + + // 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<T | undefined> { + 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, + ): Promise<T | 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 async resolveAndUpdatePaths( + workspaceFolder: Uri | undefined, + debugConfiguration: LaunchRequestArguments, + ): Promise<void> { + BaseConfigurationResolver.resolveAndUpdateEnvFilePath(workspaceFolder, debugConfiguration); + await this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); + } + + protected static resolveAndUpdateEnvFilePath( + workspaceFolder: Uri | undefined, + debugConfiguration: LaunchRequestArguments, + ): void { + if (!debugConfiguration) { + return; + } + if (debugConfiguration.envFile && (workspaceFolder || debugConfiguration.cwd)) { + debugConfiguration.envFile = resolveVariables( + debugConfiguration.envFile, + (workspaceFolder ? workspaceFolder.fsPath : undefined) || debugConfiguration.cwd, + undefined, + ); } } - protected resolveAndUpdatePythonPath(workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments): void { + + protected async resolveAndUpdatePythonPath( + workspaceFolder: Uri | undefined, + debugConfiguration: LaunchRequestArguments, + ): Promise<void> { if (!debugConfiguration) { return; } - if (debugConfiguration.pythonPath === '${config:python.pythonPath}' || !debugConfiguration.pythonPath) { - const pythonPath = this.configurationService.getSettings(workspaceFolder).pythonPath; - debugConfiguration.pythonPath = pythonPath; + if (debugConfiguration.pythonPath === '${command:python.interpreterPath}' || !debugConfiguration.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 isDebuggingFlask(debugConfiguration: Partial<LaunchRequestArguments & AttachRequestArguments>) { - return (debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK') ? true : false; + + protected static fixUpPathMappings( + pathMappings: PathMapping[], + defaultLocalRoot?: string, + defaultRemoteRoot?: string, + ): PathMapping[] { + if (!defaultLocalRoot) { + return []; + } + if (!defaultRemoteRoot) { + defaultRemoteRoot = defaultLocalRoot; + } + + if (pathMappings.length === 0) { + pathMappings = [ + { + localRoot: defaultLocalRoot, + remoteRoot: defaultRemoteRoot, + }, + ]; + } else { + // Expand ${workspaceFolder} variable first if necessary. + 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 (getOSType() === OSType.Windows) { + // TODO: Apply to remoteRoot too? + pathMappings = pathMappings.map(({ localRoot: windowsLocalRoot, remoteRoot }) => { + let localRoot = windowsLocalRoot; + if (windowsLocalRoot.match(/^[A-Z]:/)) { + localRoot = `${windowsLocalRoot[0].toLowerCase()}${windowsLocalRoot.substr(1)}`; + } + return { localRoot, remoteRoot }; + }); + } + + return pathMappings; } - protected sendTelemetry(trigger: 'launch' | 'attach', debugConfiguration: Partial<LaunchRequestArguments & AttachRequestArguments>) { - 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(DEBUGGER, undefined, telemetryProps); + + protected static isDebuggingFastAPI( + debugConfiguration: Partial<LaunchRequestArguments & AttachRequestArguments>, + ): boolean { + return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FASTAPI'); } + protected static isDebuggingFlask( + debugConfiguration: Partial<LaunchRequestArguments & AttachRequestArguments>, + ): 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 new file mode 100644 index 000000000000..15be5f97538e --- /dev/null +++ b/src/client/debugger/extension/configuration/resolvers/helper.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +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, + baseVars?: EnvironmentVariables, + ): Promise<EnvironmentVariables>; +} + +@injectable() +export class DebugEnvironmentVariablesHelper implements IDebugEnvironmentVariablesService { + constructor( + @inject(IEnvironmentVariablesService) private envParser: IEnvironmentVariablesService, + @inject(ICurrentProcess) private process: ICurrentProcess, + ) {} + + public async getEnvironmentVariables( + args: LaunchRequestArguments, + baseVars?: EnvironmentVariables, + ): Promise<EnvironmentVariables> { + const pathVariableName = getSearchPathEnvVarNames()[0]; + + // Merge variables from both .env file and env json variables. + const debugLaunchEnvVars: Record<string, string> = + args.env && Object.keys(args.env).length > 0 + ? ({ ...args.env } as Record<string, string>) + : ({} as Record<string, string>); + const envFileVars = await this.envParser.parseFile(args.envFile, debugLaunchEnvVars); + 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] ?? debugLaunchEnvVars[pathVariableName.toUpperCase()], + ); + this.envParser.appendPythonPath(env, debugLaunchEnvVars.PYTHONPATH); + + if (typeof env[pathVariableName] === 'string' && env[pathVariableName]!.length > 0) { + // Now merge this path with the current system path. + // We need to do this to ensure the PATH variable always has the system PATHs as well. + this.envParser.appendPath(env, this.process.env[pathVariableName]!); + } + if (typeof env.PYTHONPATH === 'string' && env.PYTHONPATH.length > 0) { + // We didn't have a value for PATH earlier and now we do. + // Now merge this path with the current system path. + // We need to do this to ensure the PATH variable always has the system PATHs as well. + this.envParser.appendPythonPath(env, this.process.env.PYTHONPATH!); + } + + if (args.console === 'internalConsole') { + // For debugging, when not using any terminal, then we need to provide all env variables. + // As we're spawning the process, we need to ensure all env variables are passed. + // Including those from the current process (i.e. everything, not just custom vars). + this.envParser.mergeVariables(this.process.env, env); + + if (env[pathVariableName] === undefined && typeof this.process.env[pathVariableName] === 'string') { + env[pathVariableName] = this.process.env[pathVariableName]; + } + if (env.PYTHONPATH === undefined && typeof this.process.env.PYTHONPATH === 'string') { + env.PYTHONPATH = this.process.env.PYTHONPATH; + } + } + + if (!env.hasOwnProperty('PYTHONIOENCODING')) { + env.PYTHONIOENCODING = 'UTF-8'; + } + if (!env.hasOwnProperty('PYTHONUNBUFFERED')) { + env.PYTHONUNBUFFERED = '1'; + } + + if (args.gevent) { + env.GEVENT_SUPPORT = 'True'; // this is read in pydevd_constants.py + } + + return env; + } +} + +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 4fd6feb4efc8..3ca38fb0f710 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -4,126 +4,212 @@ 'use strict'; import { inject, injectable, named } from 'inversify'; -import * as path from 'path'; 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 { IConfigurationProviderUtils } from '../types'; import { BaseConfigurationResolver } from './base'; +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<LaunchRequestArguments> { - constructor(@inject(IWorkspaceService) workspaceService: IWorkspaceService, - @inject(IDocumentManager) documentManager: IDocumentManager, - @inject(IConfigurationProviderUtils) private readonly configurationProviderUtils: IConfigurationProviderUtils, - @inject(IDiagnosticsService) @named(InvalidPythonPathInDebuggerServiceId) private readonly invalidPythonPathInDebuggerService: IInvalidPythonPathInDebuggerService, - @inject(IPlatformService) private readonly platformService: IPlatformService, - @inject(IConfigurationService) configurationService: IConfigurationService) { - super(workspaceService, documentManager, configurationService); - } - public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: LaunchRequestArguments, token?: CancellationToken): Promise<LaunchRequestArguments | undefined> { - const workspaceFolder = this.getWorkspaceFolder(folder); + private isCustomPythonSet = false; - const config = debugConfiguration as LaunchRequestArguments; - const numberOfSettings = Object.keys(config); + constructor( + @inject(IDiagnosticsService) + @named(InvalidPythonPathInDebuggerServiceId) + private readonly invalidPythonPathInDebuggerService: IInvalidPythonPathInDebuggerService, + @inject(IConfigurationService) configurationService: IConfigurationService, + @inject(IDebugEnvironmentVariablesService) private readonly debugEnvHelper: IDebugEnvironmentVariablesService, + @inject(IInterpreterService) interpreterService: IInterpreterService, + @inject(IEnvironmentActivationService) private environmentActivationService: IEnvironmentActivationService, + ) { + super(configurationService, interpreterService); + } - if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) { - const defaultProgram = this.getProgram(); + public async resolveDebugConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: LaunchRequestArguments, + _token?: CancellationToken, + ): Promise<LaunchRequestArguments | undefined> { + 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); - const isValid = await this.validateLaunchConfiguration(folder, config); + public async resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: LaunchRequestArguments, + _token?: CancellationToken, + ): Promise<LaunchRequestArguments | undefined> { + const workspaceFolder = LaunchConfigurationResolver.getWorkspaceFolder(folder); + await this.provideLaunchDefaults(workspaceFolder, debugConfiguration); + + 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): Promise<void> { - this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); + + protected async provideLaunchDefaults( + workspaceFolder: Uri | undefined, + debugConfiguration: LaunchRequestArguments, + ): Promise<void> { + 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; } if (typeof debugConfiguration.envFile !== 'string' && workspaceFolder) { - const envFile = path.join(workspaceFolder.fsPath, '.env'); - debugConfiguration.envFile = envFile; - } + const settings = this.configurationService.getSettings(workspaceFolder); + debugConfiguration.envFile = settings.envFile; + } + let baseEnvVars: EnvironmentVariables | undefined; + if (this.isCustomPythonSet || debugConfiguration.console !== 'integratedTerminal') { + // We only have the right activated environment present in integrated terminal if no custom Python path + // is specified. Otherwise, we need to explicitly set the variables. + baseEnvVars = await this.environmentActivationService.getActivatedEnvironmentVariables( + workspaceFolder, + await this.interpreterService.getInterpreterDetails(debugConfiguration.python ?? ''), + ); + } + // Extract environment variables from .env file in the vscode context and + // set the "env" debug configuration argument. This expansion should be + // done here before handing of the environment settings to the debug adapter + debugConfiguration.env = await this.debugEnvHelper.getEnvironmentVariables(debugConfiguration, baseEnvVars); + if (typeof debugConfiguration.stopOnEntry !== 'boolean') { debugConfiguration.stopOnEntry = false; } - if (typeof debugConfiguration.showReturnValue !== 'boolean') { - debugConfiguration.showReturnValue = false; - } + debugConfiguration.showReturnValue = debugConfiguration.showReturnValue !== false; if (!debugConfiguration.console) { debugConfiguration.console = 'integratedTerminal'; } // If using a terminal, then never open internal console. - if (debugConfiguration.console !== 'none' && !debugConfiguration.internalConsoleOptions) { + if (debugConfiguration.console !== 'internalConsole' && !debugConfiguration.internalConsoleOptions) { debugConfiguration.internalConsoleOptions = 'neverOpen'; } if (!Array.isArray(debugConfiguration.debugOptions)) { debugConfiguration.debugOptions = []; } - // Pass workspace folder so we can get this when we get debug events firing. - debugConfiguration.workspaceFolder = workspaceFolder ? workspaceFolder.fsPath : undefined; const debugOptions = debugConfiguration.debugOptions!; - if (debugConfiguration.debugStdLib) { - this.debugOption(debugOptions, DebugOptions.DebugStdLib); - } if (debugConfiguration.stopOnEntry) { - this.debugOption(debugOptions, DebugOptions.StopOnEntry); + LaunchConfigurationResolver.debugOption(debugOptions, DebugOptions.StopOnEntry); } if (debugConfiguration.showReturnValue) { - this.debugOption(debugOptions, DebugOptions.ShowReturnValue); + LaunchConfigurationResolver.debugOption(debugOptions, DebugOptions.ShowReturnValue); } if (debugConfiguration.django) { - this.debugOption(debugOptions, DebugOptions.Django); + LaunchConfigurationResolver.debugOption(debugOptions, DebugOptions.Django); } if (debugConfiguration.jinja) { - this.debugOption(debugOptions, DebugOptions.Jinja); + LaunchConfigurationResolver.debugOption(debugOptions, DebugOptions.Jinja); + } + if (debugConfiguration.redirectOutput === undefined && debugConfiguration.console === 'internalConsole') { + debugConfiguration.redirectOutput = true; } - if (debugConfiguration.redirectOutput || debugConfiguration.redirectOutput === undefined) { - this.debugOption(debugOptions, DebugOptions.RedirectOutput); + if (debugConfiguration.redirectOutput) { + LaunchConfigurationResolver.debugOption(debugOptions, DebugOptions.RedirectOutput); } if (debugConfiguration.sudo) { - this.debugOption(debugOptions, DebugOptions.Sudo); + LaunchConfigurationResolver.debugOption(debugOptions, DebugOptions.Sudo); } if (debugConfiguration.subProcess === true) { - this.debugOption(debugOptions, DebugOptions.SubProcess); - } - if (this.platformService.isWindows) { - this.debugOption(debugOptions, DebugOptions.FixFilePathCase); - } - const isFlask = this.isDebuggingFlask(debugConfiguration); - if ((debugConfiguration.pyramid || isFlask) - && debugOptions.indexOf(DebugOptions.Jinja) === -1 - && debugConfiguration.jinja !== false) { - this.debugOption(debugOptions, DebugOptions.Jinja); - } - if (debugConfiguration.pyramid) { - debugConfiguration.program = (await this.configurationProviderUtils.getPyramidStartupScriptFilePath(workspaceFolder))!; + LaunchConfigurationResolver.debugOption(debugOptions, DebugOptions.SubProcess); + } + if (getOSType() === OSType.Windows) { + LaunchConfigurationResolver.debugOption(debugOptions, DebugOptions.FixFilePathCase); + } + const isFastAPI = LaunchConfigurationResolver.isDebuggingFastAPI(debugConfiguration); + const isFlask = LaunchConfigurationResolver.isDebuggingFlask(debugConfiguration); + if ( + (debugConfiguration.pyramid || isFlask || isFastAPI) && + debugOptions.indexOf(DebugOptions.Jinja) === -1 && + debugConfiguration.jinja !== false + ) { + LaunchConfigurationResolver.debugOption(debugOptions, DebugOptions.Jinja); + } + // Unlike with attach, we do not set a default path mapping. + // (See: https://github.com/microsoft/vscode-python/issues/3568) + if (debugConfiguration.pathMappings) { + let { pathMappings } = debugConfiguration; + if (pathMappings.length > 0) { + pathMappings = LaunchConfigurationResolver.fixUpPathMappings( + pathMappings || [], + workspaceFolder ? workspaceFolder.fsPath : '', + ); + } + debugConfiguration.pathMappings = pathMappings.length > 0 ? pathMappings : undefined; } - this.sendTelemetry('launch', debugConfiguration); } - protected async validateLaunchConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: LaunchRequestArguments): Promise<boolean> { + protected async validateLaunchConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: LaunchRequestArguments, + ): Promise<boolean> { const diagnosticService = this.invalidPythonPathInDebuggerService; - return diagnosticService.validatePythonPath(debugConfiguration.pythonPath, 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 8a5a7473249c..eaebf6d435c4 100644 --- a/src/client/debugger/extension/configuration/types.ts +++ b/src/client/debugger/extension/configuration/types.ts @@ -3,21 +3,19 @@ 'use strict'; -import { CancellationToken, DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; -import { DebugConfigurationType, IDebugConfigurationProvider } from '../types'; - -export const IConfigurationProviderUtils = Symbol('IConfigurationProviderUtils'); - -export interface IConfigurationProviderUtils { - getPyramidStartupScriptFilePath(resource?: Uri): Promise<string | undefined>; -} +import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; export const IDebugConfigurationResolver = Symbol('IDebugConfigurationResolver'); export interface IDebugConfigurationResolver<T extends DebugConfiguration> { - resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: T, token?: CancellationToken): Promise<T | undefined>; -} + resolveDebugConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: T, + token?: CancellationToken, + ): Promise<T | undefined>; -export const IDebugConfigurationProviderFactory = Symbol('IDebugConfigurationProviderFactory'); -export interface IDebugConfigurationProviderFactory { - create(configurationType: DebugConfigurationType): IDebugConfigurationProvider; + resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: T, + token?: CancellationToken, + ): Promise<T | undefined>; } 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<void> { + 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<DebugConfiguration> { + 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/hooks/childProcessAttachHandler.ts b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts index ac9dc291d05d..233818e00aaf 100644 --- a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts +++ b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts @@ -4,28 +4,41 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { DebugSessionCustomEvent } from 'vscode'; +import { DebugConfiguration, DebugSessionCustomEvent } from 'vscode'; import { swallowExceptions } from '../../../common/utils/decorators'; -import { PTVSDEvents } from './constants'; -import { ChildProcessLaunchData, IChildProcessAttachService, IDebugSessionEventHandlers } from './types'; +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 classs responsible for multi-proc debugging. - * @export - * @class ChildProcessAttachEventHandler - * @implements {IDebugSessionEventHandlers} + * child processes launched. I.e. this is the class responsible for multi-proc debugging. */ @injectable() export class ChildProcessAttachEventHandler implements IDebugSessionEventHandlers { - constructor(@inject(IChildProcessAttachService) private readonly childProcessAttachService: IChildProcessAttachService) { } + constructor( + @inject(IChildProcessAttachService) private readonly childProcessAttachService: IChildProcessAttachService, + ) {} @swallowExceptions('Handle child process launch') public async handleCustomEvent(event: DebugSessionCustomEvent): Promise<void> { - if (!event || event.event !== PTVSDEvents.ChildProcessLaunched) { + if (!event || event.session.configuration.type !== DebuggerTypeName) { return; } - const data = event.body! as ChildProcessLaunchData; - await this.childProcessAttachService.attach(data); + + let data: AttachRequestArguments & DebugConfiguration; + if ( + event.event === DebuggerEvents.PtvsdAttachToSubprocess || + event.event === DebuggerEvents.DebugpyAttachToSubprocess + ) { + data = event.body as AttachRequestArguments & DebugConfiguration; + } else { + return; + } + + if (Object.keys(data).length > 0) { + await this.childProcessAttachService.attach(data, event.session); + } } } diff --git a/src/client/debugger/extension/hooks/childProcessAttachService.ts b/src/client/debugger/extension/hooks/childProcessAttachService.ts index 23f0db700b0a..39556f94c87c 100644 --- a/src/client/debugger/extension/hooks/childProcessAttachService.ts +++ b/src/client/debugger/extension/hooks/childProcessAttachService.ts @@ -4,52 +4,47 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { DebugConfiguration, 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 { DEBUGGER_ATTACH_TO_CHILD_PROCESS } from '../../../telemetry/constants'; import { AttachRequestArguments } from '../../types'; -import { ChildProcessLaunchData, IChildProcessAttachService } 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 classs responsible for multi-proc debugging. - * @export - * @class ChildProcessAttachEventHandler - * @implements {IChildProcessAttachService} + * child processes launched. I.e. this is the class responsible for multi-proc debugging. */ @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(DEBUGGER_ATTACH_TO_CHILD_PROCESS) - public async attach(data: ChildProcessLaunchData): Promise<void> { - const folder = this.getRelatedWorkspaceFolder(data); - const debugConfig = this.getAttachConfiguration(data); - const launched = await this.debugService.startDebugging(folder, debugConfig); + public async attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise<void> { + const debugConfig: AttachRequestArguments & DebugConfiguration = data; + const folder = this.getRelatedWorkspaceFolder(debugConfig); + 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 ${data.processId}`).then(noop, noop); + showErrorMessage(l10n.t('Failed to launch debugger for child process {0}', debugConfig.subProcessId!)).then( + noop, + noop, + ); } } - protected getRelatedWorkspaceFolder(data: ChildProcessLaunchData): WorkspaceFolder | undefined { - const workspaceFolder = data.rootStartRequest.arguments.workspaceFolder; - if (!this.workspaceService.hasWorkspaceFolders || !workspaceFolder) { + + private getRelatedWorkspaceFolder( + config: AttachRequestArguments & DebugConfiguration, + ): WorkspaceFolder | undefined { + const workspaceFolder = config.workspaceFolder; + + const hasWorkspaceFolders = (getWorkspaceFolders()?.length || 0) > 0; + if (!hasWorkspaceFolders || !workspaceFolder) { return; } - return this.workspaceService.workspaceFolders!.find(ws => ws.uri.fsPath === workspaceFolder); - } - protected getAttachConfiguration(data: ChildProcessLaunchData): AttachRequestArguments & DebugConfiguration { - const args = data.rootStartRequest.arguments; - // tslint:disable-next-line:no-any - const config = JSON.parse(JSON.stringify(args)) as any as (AttachRequestArguments & DebugConfiguration); - - config.host = args.request === 'attach' ? args.host! : 'localhost'; - config.port = data.port; - config.name = `Child Process ${data.processId}`; - config.request = 'attach'; - return config; + 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 b956a06f74d8..3bd0b657281e 100644 --- a/src/client/debugger/extension/hooks/constants.ts +++ b/src/client/debugger/extension/hooks/constants.ts @@ -3,7 +3,8 @@ 'use strict'; -export enum PTVSDEvents { +export enum DebuggerEvents { // Event sent by PTVSD when a child process is launched and ready to be attached to for multi-proc debugging. - ChildProcessLaunched = 'ptvsd_subprocess' + PtvsdAttachToSubprocess = 'ptvsd_attach', + DebugpyAttachToSubprocess = 'debugpyAttach', } diff --git a/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts b/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts index 0837a50c9e7c..7b1dd1516abd 100644 --- a/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts +++ b/src/client/debugger/extension/hooks/eventHandlerDispatcher.ts @@ -9,15 +9,25 @@ import { IDisposableRegistry } from '../../../common/types'; import { IDebugSessionEventHandlers } from './types'; export class DebugSessionEventDispatcher { - constructor(@multiInject(IDebugSessionEventHandlers) private readonly eventHandlers: IDebugSessionEventHandlers[], + 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); - })); - this.disposables.push(this.debugService.onDidTerminateDebugSession(e => { - this.eventHandlers.forEach(handler => handler.handleTerminateEvent ? handler.handleTerminateEvent(e).ignoreErrors() : undefined); - })); + this.disposables.push( + this.debugService.onDidReceiveDebugSessionCustomEvent((e) => { + this.eventHandlers.forEach((handler) => + handler.handleCustomEvent ? handler.handleCustomEvent(e).ignoreErrors() : undefined, + ); + }), + ); + this.disposables.push( + this.debugService.onDidTerminateDebugSession((e) => { + this.eventHandlers.forEach((handler) => + handler.handleTerminateEvent ? handler.handleTerminateEvent(e).ignoreErrors() : undefined, + ); + }), + ); } } diff --git a/src/client/debugger/extension/hooks/types.ts b/src/client/debugger/extension/hooks/types.ts index b62ae91b2998..80d393057fb4 100644 --- a/src/client/debugger/extension/hooks/types.ts +++ b/src/client/debugger/extension/hooks/types.ts @@ -3,8 +3,8 @@ 'use strict'; -import { DebugSession, DebugSessionCustomEvent } from 'vscode'; -import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; +import { DebugConfiguration, DebugSession, DebugSessionCustomEvent } from 'vscode'; +import { AttachRequestArguments } from '../../types'; export const IDebugSessionEventHandlers = Symbol('IDebugSessionEventHandlers'); export interface IDebugSessionEventHandlers { @@ -12,50 +12,7 @@ export interface IDebugSessionEventHandlers { handleTerminateEvent?(e: DebugSession): Promise<void>; } -export type ChildProcessLaunchData = { - /** - * The main process (that in turn starts child processes). - * @type {number} - */ - rootProcessId: number; - /** - * The immediate parent of the current process (identified by `processId`). - * This could be the same as `parentProcessId`, or something else. - * @type {number} - */ - parentProcessId: number; - /** - * The process id of the child process launched. - * @type {number} - */ - processId: number; - /** - * Port on which the child process is listening and waiting for the debugger to attach. - * @type {number} - */ - port: number; - /** - * The request object sent to the PTVSD by the main process. - * If main process was launched, then `arguments` would be the launch request arsg, - * else it would be the attach request args. - * @type {({ - * // tslint:disable-next-line:no-banned-terms - * arguments: LaunchRequestArguments | AttachRequestArguments; - * command: 'attach' | 'request'; - * seq: number; - * type: string; - * })} - */ - rootStartRequest: { - // tslint:disable-next-line:no-banned-terms - arguments: LaunchRequestArguments | AttachRequestArguments; - command: 'attach' | 'request'; - seq: number; - type: string; - }; -}; - export const IChildProcessAttachService = Symbol('IChildProcessAttachService'); export interface IChildProcessAttachService { - attach(data: ChildProcessLaunchData): Promise<void>; + attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise<void>; } diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index a3fed3aab68e..7734e87124cd 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -3,39 +3,68 @@ 'use strict'; +import { IExtensionSingleActivationService } from '../../activation/types'; import { IServiceManager } from '../../ioc/types'; import { AttachRequestArguments, LaunchRequestArguments } from '../types'; -import { DebuggerBanner } from './banner'; -import { ConfigurationProviderUtils } from './configuration/configurationProviderUtils'; +import { DebugAdapterActivator } from './adapter/activator'; +import { DebugAdapterDescriptorFactory } from './adapter/factory'; +import { DebugSessionLoggingFactory } from './adapter/logging'; +import { OutdatedDebuggerPromptFactory } from './adapter/outdatedDebuggerPrompt'; +import { AttachProcessProviderFactory } from './attachQuickPick/factory'; +import { IAttachProcessProviderFactory } from './attachQuickPick/types'; import { PythonDebugConfigurationService } from './configuration/debugConfigurationService'; -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 { 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 { IConfigurationProviderUtils, 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, IDebugConfigurationProvider, IDebugConfigurationService, IDebuggerBanner } from './types'; +import { + IDebugAdapterDescriptorFactory, + IDebugConfigurationService, + IDebugSessionLoggingFactory, + IOutdatedDebuggerPromptFactory, +} from './types'; -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton<IDebugConfigurationService>(IDebugConfigurationService, PythonDebugConfigurationService); - serviceManager.addSingleton<IConfigurationProviderUtils>(IConfigurationProviderUtils, ConfigurationProviderUtils); - serviceManager.addSingleton<IDebuggerBanner>(IDebuggerBanner, DebuggerBanner); +export function registerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingleton<IDebugConfigurationService>( + IDebugConfigurationService, + PythonDebugConfigurationService, + ); serviceManager.addSingleton<IChildProcessAttachService>(IChildProcessAttachService, ChildProcessAttachService); serviceManager.addSingleton<IDebugSessionEventHandlers>(IDebugSessionEventHandlers, ChildProcessAttachEventHandler); - serviceManager.addSingleton<IDebugConfigurationResolver<LaunchRequestArguments>>(IDebugConfigurationResolver, LaunchConfigurationResolver, 'launch'); - serviceManager.addSingleton<IDebugConfigurationResolver<AttachRequestArguments>>(IDebugConfigurationResolver, AttachConfigurationResolver, 'attach'); - serviceManager.addSingleton<IDebugConfigurationProviderFactory>(IDebugConfigurationProviderFactory, DebugConfigurationProviderFactory); - serviceManager.addSingleton<IDebugConfigurationProvider>(IDebugConfigurationProvider, FileLaunchDebugConfigurationProvider, DebugConfigurationType.launchFile); - serviceManager.addSingleton<IDebugConfigurationProvider>(IDebugConfigurationProvider, DjangoLaunchDebugConfigurationProvider, DebugConfigurationType.launchDjango); - serviceManager.addSingleton<IDebugConfigurationProvider>(IDebugConfigurationProvider, FlaskLaunchDebugConfigurationProvider, DebugConfigurationType.launchFlask); - serviceManager.addSingleton<IDebugConfigurationProvider>(IDebugConfigurationProvider, RemoteAttachDebugConfigurationProvider, DebugConfigurationType.remoteAttach); - serviceManager.addSingleton<IDebugConfigurationProvider>(IDebugConfigurationProvider, ModuleLaunchDebugConfigurationProvider, DebugConfigurationType.launchModule); - serviceManager.addSingleton<IDebugConfigurationProvider>(IDebugConfigurationProvider, PyramidLaunchDebugConfigurationProvider, DebugConfigurationType.launchPyramid); + serviceManager.addSingleton<IDebugConfigurationResolver<LaunchRequestArguments>>( + IDebugConfigurationResolver, + LaunchConfigurationResolver, + 'launch', + ); + serviceManager.addSingleton<IDebugConfigurationResolver<AttachRequestArguments>>( + IDebugConfigurationResolver, + AttachConfigurationResolver, + 'attach', + ); + serviceManager.addSingleton<IDebugEnvironmentVariablesService>( + IDebugEnvironmentVariablesService, + DebugEnvironmentVariablesHelper, + ); + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + DebugAdapterActivator, + ); + serviceManager.addSingleton<IDebugAdapterDescriptorFactory>( + IDebugAdapterDescriptorFactory, + DebugAdapterDescriptorFactory, + ); + serviceManager.addSingleton<IDebugSessionLoggingFactory>(IDebugSessionLoggingFactory, DebugSessionLoggingFactory); + serviceManager.addSingleton<IOutdatedDebuggerPromptFactory>( + IOutdatedDebuggerPromptFactory, + OutdatedDebuggerPromptFactory, + ); + serviceManager.addSingleton<IAttachProcessProviderFactory>( + IAttachProcessProviderFactory, + AttachProcessProviderFactory, + ); + serviceManager.addSingleton<IExtensionSingleActivationService>(IExtensionSingleActivationService, DebugCommands); } diff --git a/src/client/debugger/extension/types.ts b/src/client/debugger/extension/types.ts index 6b89701121f0..4a8f35e2b808 100644 --- a/src/client/debugger/extension/types.ts +++ b/src/client/debugger/extension/types.ts @@ -3,29 +3,23 @@ 'use strict'; -import { CancellationToken, DebugConfigurationProvider, 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 interface IDebugConfigurationService extends DebugConfigurationProvider {} -export const IDebugConfigurationProvider = Symbol('IDebugConfigurationProvider'); -export type DebugConfigurationState = { config: Partial<DebugConfigurationArguments>; folder?: WorkspaceFolder; token?: CancellationToken }; -export interface IDebugConfigurationProvider { - buildConfiguration(input: MultiStepInput<DebugConfigurationState>, state: DebugConfigurationState): Promise<InputStep<DebugConfigurationState> | void>; -} +export const IDebugAdapterDescriptorFactory = Symbol('IDebugAdapterDescriptorFactory'); +export interface IDebugAdapterDescriptorFactory extends DebugAdapterDescriptorFactory {} + +export const IDebugSessionLoggingFactory = Symbol('IDebugSessionLoggingFactory'); + +export interface IDebugSessionLoggingFactory extends DebugAdapterTrackerFactory {} + +export const IOutdatedDebuggerPromptFactory = Symbol('IOutdatedDebuggerPromptFactory'); + +export interface IOutdatedDebuggerPromptFactory extends DebugAdapterTrackerFactory {} -export enum DebugConfigurationType { - default = 'default', - launchFile = 'launchFile', - remoteAttach = 'remoteAttach', - launchDjango = 'launchDjango', - launchFlask = 'launchFlask', - launchModule = 'launchModule', - launchPyramid = 'launchPyramid' +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<string>; + }; +} + +async function activateExtension() { + const extension = extensions.getExtension('ms-python.debugpy'); + if (extension) { + if (!extension.isActive) { + await extension.activate(); + } + } + return extension; +} + +async function getPythonDebuggerExtensionAPI(): Promise<IPythonDebuggerExtensionApi | undefined> { + const extension = await activateExtension(); + return extension?.exports as IPythonDebuggerExtensionApi; +} + +export async function getDebugpyPath(): Promise<string> { + const api = await getPythonDebuggerExtensionAPI(); + return api?.debug.getDebuggerPackagePath() ?? ''; +} diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts index 50eec0f94a92..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,7 +18,28 @@ 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; +}; +type Connection = { + host?: string; + port?: number; +}; + +export interface IAutomaticCodeReload { + enable?: boolean; + exclude?: string[]; + include?: string[]; + pollingInterval?: number; } interface ICommonDebugArguments { @@ -28,6 +48,7 @@ interface ICommonDebugArguments { gevent?: boolean; jinja?: boolean; debugStdLib?: boolean; + justMyCode?: boolean; logToFile?: boolean; debugOptions?: DebugOptions[]; port?: number; @@ -35,44 +56,84 @@ interface ICommonDebugArguments { // Show return values of functions while stepping. showReturnValue?: boolean; 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; - // An absolute path to local directory with source. + customDebugger?: boolean; + // localRoot and remoteRoot are deprecated (replaced by pathMappings). localRoot?: string; remoteRoot?: string; - pathMappings?: { localRoot: string; remoteRoot: string }[]; - customDebugger?: boolean; + + // Internal field used to attach to subprocess using python debug adapter + subProcessId?: number; + + processId?: number | string; + connect?: Connection; + listen?: Connection; } -export interface IKnownLaunchRequestArguments extends ICommonDebugArguments { +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?: { [key: string]: string | undefined }; - envFile: string; + env?: Record<string, string | undefined>; + envFile?: string; console?: ConsoleType; + + // 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 <debugAdapterPath>"; + // 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; + +export interface LaunchRequestArguments + extends DebugProtocol.LaunchRequestArguments, + IKnownLaunchRequestArguments, + DebugConfiguration { + type: typeof DebuggerTypeName | typeof PythonDebuggerTypeName; } -// tslint:disable-next-line:interface-name -export interface AttachRequestArguments extends DebugProtocol.AttachRequestArguments, IKnownAttachDebugArguments, DebugConfiguration { - type: typeof DebuggerTypeName; +export interface AttachRequestArguments + extends DebugProtocol.AttachRequestArguments, + IKnownAttachDebugArguments, + DebugConfiguration { + type: typeof DebuggerTypeName | typeof PythonDebuggerTypeName; } -// tslint:disable-next-line:interface-name -export interface DebugConfigurationArguments extends LaunchRequestArguments, AttachRequestArguments { } +export interface DebugConfigurationArguments extends LaunchRequestArguments, AttachRequestArguments {} + +export type ConsoleType = 'internalConsole' | 'integratedTerminal' | 'externalTerminal'; -export type ConsoleType = 'none' | 'integratedTerminal' | 'externalTerminal'; +export type TriggerType = 'launch' | 'attach' | 'test'; 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<EnvironmentsChangedParams[]>(); +/** + * @deprecated Will be removed soon. + */ +export function reportInterpretersChanged(e: EnvironmentsChangedParams[]): void { + onDidInterpretersChangedEvent.fire(e); +} + +const onDidActiveInterpreterChangedEvent = new EventEmitter<ActiveEnvironmentChangedParams>(); +/** + * @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>(IInterpreterPathService); + const interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService); + const extensions = serviceContainer.get<IExtensions>(IExtensions); + const warningLogged = new Set<string>(); + 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<EnvironmentDetails | undefined> { + 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<void> { + 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<void> | 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<string, unknown>; +} + +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<Uri | undefined>; + /** + * 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, + * * `['<path to the interpreter set in settings>']` + * * `['<path to the interpreter selected by the extension when setting is not set>']` + * * `['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<EnvPathType | undefined>; + /** + * 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<EnvironmentDetails | undefined>; + /** + * 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<EnvPathType[] | undefined>; + /** + * 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<void>; + /** + * 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<EnvPathType[] | undefined>; + /** + * 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<ProgressNotificationEvent>; + /** + * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active + * refreshes going on. + */ + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | 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<EnvironmentsChangedParams[]>; + /** + * @deprecated Use {@link ProposedExtensionAPI.environments} `onDidChangeActiveEnvironmentPath` instead. This will soon be removed. + */ + onDidActiveEnvironmentChanged: Event<ActiveEnvironmentChangedParams>; + }; +} 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<boolean>('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<boolean>('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<boolean>('useEnvironmentsExtension', false) ?? false; + // If extension is installed and in experiment, then use it. + _useExt = !!getExtension(ENVS_EXTENSION_ID) && inExpSetting; + return _useExt; +} + +const onDidChangeEnvironmentEnvExtEmitter: EventEmitter<DidChangeEnvironmentEventArgs> = 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<PythonEnvironmentApi> { + 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<PythonProcess> { + const envExtApi = await getEnvExtApi(); + return envExtApi.runInBackground(environment, options); +} + +export async function getEnvironment(scope: GetEnvironmentScope): Promise<PythonEnvironment | undefined> { + const envExtApi = await getEnvExtApi(); + const env = await envExtApi.getEnvironment(scope); + if (!env) { + traceLog(Interpreters.envExtNoActiveEnvironment); + } + return env; +} + +export async function resolveEnvironment(pythonPath: string): Promise<PythonEnvironment | undefined> { + const envExtApi = await getEnvExtApi(); + return envExtApi.resolveEnvironment(Uri.file(pythonPath)); +} + +export async function refreshEnvironments(scope: RefreshEnvironmentsScope): Promise<void> { + const envExtApi = await getEnvExtApi(); + return envExtApi.refreshEnvironments(scope); +} + +export async function runInTerminal( + resource: Uri | undefined, + args?: string[], + cwd?: string | Uri, + show?: boolean, +): Promise<Terminal> { + 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<Terminal> { + 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<void> { + 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<string, PythonEnvironment | undefined>(); +export async function getActiveInterpreterLegacy(resource?: Uri): Promise<PythonEnvironmentLegacy | undefined> { + 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<void> { + 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<void> { + const api = await getEnvExtApi(); + await api.setEnvironment(uri, undefined); +} + +export async function ensureTerminalLegacy( + resource: Uri | undefined, + options?: PythonTerminalCreateOptions, +): Promise<Terminal> { + 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<ProgressNotificationEvent>; + + private _onChanged: EventEmitter<PythonEnvCollectionChangedEvent>; + + private _refreshPromise?: Deferred<void>; + + private _envs: PythonEnvInfo[] = []; + + refreshState: ProgressReportStage; + + private _disposables: Disposable[] = []; + + constructor(private envExtApi: PythonEnvironmentApi) { + this._onProgress = new EventEmitter<ProgressNotificationEvent>(); + this._onChanged = new EventEmitter<PythonEnvCollectionChangedEvent>(); + + 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<ProgressNotificationEvent>; + + onChanged: Event<PythonEnvCollectionChangedEvent>; + + getRefreshPromise(_options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined { + return this._refreshPromise?.promise; + } + + triggerRefresh(_query?: PythonLocatorQuery, _options?: TriggerRefreshOptions): Promise<void> { + 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<PythonEnvInfo | undefined> { + 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<EnvExtApis> { + 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<string, PythonCommandRunConfiguration[]>; + + /** + * 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<string, PythonCommandRunConfiguration[]>; +} + +/** + * 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}. + * `<publisher-id>.<extension-id>:<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<PythonEnvironment | undefined>; + + /** + * 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<void>; + + /** + * 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<void>; + + /** + * 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<PythonEnvironment[]>; + + /** + * Event that is fired when the list of Python environments changes. + */ + onDidChangeEnvironments?: Event<DidChangeEnvironmentsEventArgs>; + + /** + * 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<void>; + + /** + * 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<PythonEnvironment | undefined>; + + /** + * Event that is fired when the current Python environment changes. + */ + onDidChangeEnvironment?: Event<DidChangeEnvironmentEventArgs>; + + /** + * 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<PythonEnvironment | undefined>; + + /** + * Clears the environment manager's cache. + * + * @returns A promise that resolves when the cache is cleared. + */ + clearCache?(): Promise<void>; +} + +/** + * 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<void>; + + /** + * 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<void>; + + /** + * 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<Package[] | undefined>; + + /** + * Event that is fired when packages change. + */ + onDidChangePackages?: Event<DidChangePackagesEventArgs>; + + /** + * Clears the package manager's cache. + * @returns A promise that resolves when the cache is cleared. + */ + clearCache?(): Promise<void>; +} + +/** + * 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<PythonProject | PythonProject[] | Uri | Uri[] | undefined>; + + /** + * 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<PythonEnvironment | undefined>; + + /** + * 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<void>; +} + +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<void>; + + /** + * 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<PythonEnvironment[]>; + + /** + * Event that is fired when the list of Python environments changes. + * @see {@link DidChangeEnvironmentsEventArgs} + */ + onDidChangeEnvironments: Event<DidChangeEnvironmentsEventArgs>; + + /** + * 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<PythonEnvironment | undefined>; +} + +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<void>; + + /** + * 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<PythonEnvironment | undefined>; + + /** + * Event that is fired when the selected Python environment changes for Project, Folder or File. + * @see {@link DidChangeEnvironmentEventArgs} + */ + onDidChangeEnvironment: Event<DidChangeEnvironmentEventArgs>; +} + +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<void>; + + /** + * 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<Package[] | undefined>; + + /** + * Event raised when the list of packages in a Python Environment changes. + * @see {@link DidChangePackagesEventArgs} + */ + onDidChangePackages: Event<DidChangePackagesEventArgs>; +} + +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<void>; +} + +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<DidChangePythonProjectsEventArgs>; +} + +/** + * 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<Terminal>; +} + +/** + * 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<Terminal>; + + /** + * 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<Terminal>; +} + +/** + * 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<TaskExecution>; +} + +/** + * 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<PythonProcess>; +} + +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<DidChangeEnvironmentVariablesEventArgs>; +} + +/** + * 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<ActiveEnvironmentPathChangeEvent>(); +const previousEnvMap = new Map<string, string>(); +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<EnvironmentsChangeEvent>(); +const onEnvironmentVariablesChanged = new EventEmitter<EnvironmentVariablesChangeEvent>(); +const environmentsReference = new Map<string, EnvironmentReference>(); + +/** + * Make all properties in T mutable. + */ +type Mutable<T> = { + -readonly [P in keyof T]: Mutable<T[P]>; +}; + +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>(IInterpreterPathService); + const configService = serviceContainer.get<IConfigurationService>(IConfigurationService); + const disposables = serviceContainer.get<IDisposableRegistry>(IDisposableRegistry); + const extensions = serviceContainer.get<IExtensions>(IExtensions); + const envVarsProvider = serviceContainer.get<IEnvironmentVariablesProvider>(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<void> { + 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>(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<ResolvedEnvironment | undefined> { + 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<Environment>; + 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 4b12e5156b86..c3fb2a3ab3b0 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -1,340 +1,212 @@ 'use strict'; + // This line should always be right on top. -// tslint:disable-next-line:no-any + if ((Reflect as any).metadata === undefined) { - // tslint:disable-next-line:no-require-imports no-var-requires require('reflect-metadata'); } -const durations: { [key: string]: number } = {}; + +//=============================================== +// 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 = {} 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(); -import { Container } from 'inversify'; -import { - CodeActionKind, - debug, - DebugConfigurationProvider, - Disposable, - ExtensionContext, - extensions, - IndentAction, - languages, - Memento, - OutputChannel, - ProgressLocation, - ProgressOptions, - window -} from 'vscode'; - -import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; -import { IExtensionActivationService } from './activation/types'; -import { buildApi, IExtensionApi } from './api'; -import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; -import { IApplicationDiagnostics } from './application/types'; -import { DebugService } from './common/application/debugService'; -import { IWorkspaceService } from './common/application/types'; -import { isTestExecution, PYTHON, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from './common/constants'; -import { registerTypes as registerDotNetTypes } from './common/dotnet/serviceRegistry'; -import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; -import { registerTypes as platformRegisterTypes } from './common/platform/serviceRegistry'; -import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; -import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; -import { ITerminalHelper } from './common/terminal/types'; -import { - GLOBAL_MEMENTO, - IAsyncDisposableRegistry, - IConfigurationService, - IDisposableRegistry, - IExtensionContext, - IFeatureDeprecationManager, - ILogger, - IMemento, - IOutputChannel, - Resource, - WORKSPACE_MEMENTO -} from './common/types'; -import { createDeferred } from './common/utils/async'; -import { Common } from './common/utils/localize'; -import { registerTypes as variableRegisterTypes } from './common/variables/serviceRegistry'; -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 { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService } from './interpreter/autoSelection/types'; -import { IInterpreterSelector } from './interpreter/configuration/types'; -import { - ICondaService, - IInterpreterLocatorProgressService, - IInterpreterService, - InterpreterLocatorProgressHandler, - PythonInterpreter -} from './interpreter/contracts'; -import { registerTypes as interpretersRegisterTypes } from './interpreter/serviceRegistry'; -import { ServiceContainer } from './ioc/container'; -import { ServiceManager } from './ioc/serviceManager'; -import { IServiceContainer, IServiceManager } from './ioc/types'; -import { LinterCommands } from './linters/linterCommands'; -import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; -import { ILintingEngine } from './linters/types'; -import { PythonCodeActionProvider } from './providers/codeActionsProvider'; -import { PythonFormattingEditProvider } from './providers/formatProvider'; -import { LinterProvider } from './providers/linterProvider'; -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 { activateUpdateSparkLibraryProvider } from './providers/updateSparkLibraryProvider'; -import { sendTelemetryEvent } from './telemetry'; -import { EDITOR_LOAD } from './telemetry/constants'; -import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry'; -import { ICodeExecutionManager, ITerminalAutoActivation } from './terminals/types'; -import { TEST_OUTPUT_CHANNEL } from './unittests/common/constants'; -import { registerTypes as unitTestsRegisterTypes } from './unittests/serviceRegistry'; - -durations.codeLoadingTime = stopWatch.elapsedTime; -const activationDeferred = createDeferred<void>(); -let activatedServiceContainer: ServiceContainer | undefined; - -// tslint:disable-next-line:max-func-body-length -export async function activate(context: ExtensionContext): Promise<IExtensionApi> { - displayProgress(activationDeferred.promise); - durations.startActivateTime = stopWatch.elapsedTime; - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - const serviceContainer = new ServiceContainer(cont); - activatedServiceContainer = serviceContainer; - registerServices(context, serviceManager, serviceContainer); - initializeServices(context, serviceManager, serviceContainer); - - const autoSelection = serviceContainer.get<IInterpreterAutoSelectionService>(IInterpreterAutoSelectionService); - await autoSelection.autoSelectInterpreter(undefined); - - // When testing, do not perform health checks, as modal dialogs can be displayed. - if (!isTestExecution()) { - const appDiagnostics = serviceContainer.get<IApplicationDiagnostics>(IApplicationDiagnostics); - await appDiagnostics.performPreStartupHealthCheck(); - } - serviceManager.get<ITerminalAutoActivation>(ITerminalAutoActivation).register(); - const configuration = serviceManager.get<IConfigurationService>(IConfigurationService); - const pythonSettings = configuration.getSettings(); - - const standardOutputChannel = serviceContainer.get<OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceContainer); - - const activationService = serviceContainer.get<IExtensionActivationService>(IExtensionActivationService); - const lsActivationPromise = activationService.activate(); - displayProgress(lsActivationPromise); - - const sortImports = serviceContainer.get<ISortImportsEditingProvider>(ISortImportsEditingProvider); - sortImports.registerCommands(); - - serviceManager.get<ICodeExecutionManager>(ICodeExecutionManager).registerCommands(); - sendStartupTelemetry(Promise.all([activationDeferred.promise, lsActivationPromise]), serviceContainer).ignoreErrors(); - - const workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService); - const interpreterManager = serviceContainer.get<IInterpreterService>(IInterpreterService); - interpreterManager.refresh(workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri : undefined) - .catch(ex => console.error('Python Extension: interpreterManager.refresh', ex)); - - const jupyterExtension = extensions.getExtension('donjayamanne.jupyter'); - const lintingEngine = serviceManager.get<ILintingEngine>(ILintingEngine); - lintingEngine.linkJupyterExtension(jupyterExtension).ignoreErrors(); - - // Activate data science features - const dataScience = serviceManager.get<IDataScience>(IDataScience); - dataScience.activate().ignoreErrors(); - - context.subscriptions.push(new LinterCommands(serviceManager)); - const linterProvider = new LinterProvider(context, serviceManager); - context.subscriptions.push(linterProvider); - - // Enable indentAction - // tslint:disable-next-line:no-non-null-assertion - languages.setLanguageConfiguration(PYTHON_LANGUAGE, { - onEnterRules: [ - { - beforeText: /^\s*(?:def|class|for|if|elif|else|while|try|with|finally|except|async)\b.*:\s*/, - action: { indentAction: IndentAction.Indent } - }, - { - beforeText: /^(?!\s+\\)[^#\n]+\\\s*/, - action: { indentAction: IndentAction.Indent } - }, - { - beforeText: /^\s*#.*/, - afterText: /.+$/, - action: { indentAction: IndentAction.None, appendText: '# ' } - }, - { - beforeText: /^\s+(continue|break|return)\b.*/, - afterText: /\s+$/, - action: { indentAction: IndentAction.Outdent } - } - ] - }); - - if (pythonSettings && pythonSettings.formatting && pythonSettings.formatting.provider !== 'none') { - const formatProvider = new PythonFormattingEditProvider(context, serviceContainer); - context.subscriptions.push(languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); - context.subscriptions.push(languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); - } +// Initialize file logging here. This should not depend on too many things. +import { initializeFileLogging, traceError } from './logging'; +const logDispose: { dispose: () => void }[] = []; +initializeFileLogging(logDispose); - const deprecationMgr = serviceContainer.get<IFeatureDeprecationManager>(IFeatureDeprecationManager); - deprecationMgr.initialize(); - context.subscriptions.push(deprecationMgr); +//=============================================== +// loading starts here - context.subscriptions.push(activateUpdateSparkLibraryProvider()); +import { ProgressLocation, ProgressOptions, window } from 'vscode'; +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, 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'; - context.subscriptions.push(new ReplProvider(serviceContainer)); - context.subscriptions.push(new TerminalProvider(serviceContainer)); +durations.codeLoadingTime = stopWatch.elapsedTime; - context.subscriptions.push(languages.registerCodeActionsProvider(PYTHON, new PythonCodeActionProvider(), { providedCodeActionKinds: [CodeActionKind.SourceOrganizeImports] })); +//=============================================== +// loading ends here - serviceContainer.getAll<DebugConfigurationProvider>(IDebugConfigurationService).forEach(debugConfigProvider => { - context.subscriptions.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); - }); +// These persist between activations: +let activatedServiceContainer: IServiceContainer | undefined; - serviceContainer.get<IDebuggerBanner>(IDebuggerBanner).initialize(); - durations.endActivateTime = stopWatch.elapsedTime; - activationDeferred.resolve(); +///////////////////////////// +// public functions - const api = buildApi(Promise.all([activationDeferred.promise, lsActivationPromise])); - // 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 +export async function activate(context: IExtensionContext): Promise<PythonExtension> { + let api: PythonExtension; + let ready: Promise<void>; + 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 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, isFirstSession) + // Run in the background. + .ignoreErrors(); return api; } -export function deactivate(): Thenable<void> { +export async function deactivate(): Promise<void> { // Make sure to shutdown anybody who needs it. if (activatedServiceContainer) { - const registry = activatedServiceContainer.get<IAsyncDisposableRegistry>(IAsyncDisposableRegistry); - if (registry) { - return registry.dispose(); - } + const disposables = activatedServiceContainer.get<IDisposableRegistry>(IDisposableRegistry); + await disposeAll(disposables); + // Remove everything that is already disposed. + while (disposables.pop()); } +} + +///////////////////////////// +// activation helpers - return Promise.resolve(); +async function activateUnsafe( + context: IExtensionContext, + startupStopWatch: StopWatch, + startupDurations: IStartupDurations, +): Promise<[PythonExtension & ProposedExtensionAPI, Promise<void>, IServiceContainer]> { + // Add anything that we got from initializing logs to dispose. + context.subscriptions.push(...logDispose); + + const activationDeferred = createDeferred<void>(); + displayProgress(activationDeferred.promise); + startupDurations.startActivateTime = startupStopWatch.elapsedTime; + const activationStopWatch = new StopWatch(); + + //=============================================== + // activation starts here + + // 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>(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.totalActivateTime = startupStopWatch.elapsedTime - startupDurations.startActivateTime; + activationDeferred.resolve(); + + setTimeout(async () => { + if (activatedServiceContainer) { + const workspaceService = activatedServiceContainer.get<IWorkspaceService>(IWorkspaceService); + if (workspaceService.isTrusted) { + const interpreterManager = activatedServiceContainer.get<IInterpreterService>(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>(IRecommendedEnvironmentService) + .registerEnvApi(api.environments); + return [{ ...api, ...proposedApi }, activationPromise, ext.legacyIOC.serviceContainer]; } -// tslint:disable-next-line:no-any function displayProgress(promise: Promise<any>) { - const progressOptions: ProgressOptions = { location: ProgressLocation.Window, title: Common.loadingExtension() }; + const progressOptions: ProgressOptions = { location: ProgressLocation.Window, title: Common.loadingExtension }; window.withProgress(progressOptions, () => promise); } -function registerServices(context: ExtensionContext, serviceManager: ServiceManager, serviceContainer: ServiceContainer) { - serviceManager.addSingletonInstance<IServiceContainer>(IServiceContainer, serviceContainer); - serviceManager.addSingletonInstance<IServiceManager>(IServiceManager, serviceManager); - serviceManager.addSingletonInstance<Disposable[]>(IDisposableRegistry, context.subscriptions); - serviceManager.addSingletonInstance<Memento>(IMemento, context.globalState, GLOBAL_MEMENTO); - serviceManager.addSingletonInstance<Memento>(IMemento, context.workspaceState, WORKSPACE_MEMENTO); - serviceManager.addSingletonInstance<IExtensionContext>(IExtensionContext, context); - - const standardOutputChannel = window.createOutputChannel('Python'); - const unitTestOutChannel = window.createOutputChannel('Python Test Log'); - serviceManager.addSingletonInstance<OutputChannel>(IOutputChannel, standardOutputChannel, STANDARD_OUTPUT_CHANNEL); - serviceManager.addSingletonInstance<OutputChannel>(IOutputChannel, unitTestOutChannel, TEST_OUTPUT_CHANNEL); - - activationRegisterTypes(serviceManager); - commonRegisterTypes(serviceManager); - registerDotNetTypes(serviceManager); - processRegisterTypes(serviceManager); - variableRegisterTypes(serviceManager); - unitTestsRegisterTypes(serviceManager); - lintersRegisterTypes(serviceManager); - interpretersRegisterTypes(serviceManager); - formattersRegisterTypes(serviceManager); - platformRegisterTypes(serviceManager); - installerRegisterTypes(serviceManager); - commonRegisterTerminalTypes(serviceManager); - dataScienceRegisterTypes(serviceManager); - debugConfigurationRegisterTypes(serviceManager); - appRegisterTypes(serviceManager); - providersRegisterTypes(serviceManager); +///////////////////////////// +// error handling + +async function handleError(ex: Error, startupDurations: IStartupDurations) { + notifyUser( + "Extension activation failed, run the 'Developer: Toggle Developer Tools' command for more information.", + ); + traceError('extension activation failed', ex); + + await sendErrorTelemetry(ex, startupDurations, activatedServiceContainer); } -function initializeServices(context: ExtensionContext, serviceManager: ServiceManager, serviceContainer: ServiceContainer) { - const selector = serviceContainer.get<IInterpreterSelector>(IInterpreterSelector); - selector.initialize(); - context.subscriptions.push(selector); - - const interpreterManager = serviceContainer.get<IInterpreterService>(IInterpreterService); - interpreterManager.initialize(); - - const handlers = serviceManager.getAll<IDebugSessionEventHandlers>(IDebugSessionEventHandlers); - const disposables = serviceManager.get<IDisposableRegistry>(IDisposableRegistry); - const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); - dispatcher.registerEventHandlers(); - - // Display progress of interpreter refreshes only after extension has activated. - serviceContainer.get<InterpreterLocatorProgressHandler>(InterpreterLocatorProgressHandler).register(); - serviceContainer.get<IInterpreterLocatorProgressService>(IInterpreterLocatorProgressService).register(); - serviceContainer.get<IApplicationDiagnostics>(IApplicationDiagnostics).register(); - - // Get latest interpreter list. - const workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService); - const mainWorkspaceUri = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri : undefined; - const interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService); - interpreterService.getInterpreters(mainWorkspaceUri).ignoreErrors(); +interface IAppShell { + showErrorMessage(string: string): Promise<void>; } -// tslint:disable-next-line:no-any -async function sendStartupTelemetry(activatedPromise: Promise<any>, serviceContainer: IServiceContainer) { - const logger = serviceContainer.get<ILogger>(ILogger); +function notifyUser(msg: string) { try { - await activatedPromise; - const terminalHelper = serviceContainer.get<ITerminalHelper>(ITerminalHelper); - const terminalShellType = terminalHelper.identifyTerminalShell(terminalHelper.getTerminalShellPath()); - const condaLocator = serviceContainer.get<ICondaService>(ICondaService); - const interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService); - const workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService); - const configurationService = serviceContainer.get<IConfigurationService>(IConfigurationService); - const mainWorkspaceUri = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri : undefined; - const settings = configurationService.getSettings(mainWorkspaceUri); - const [condaVersion, interpreter, interpreters] = await Promise.all([ - condaLocator.getCondaVersion().then(ver => ver ? ver.raw : '').catch<string>(() => ''), - interpreterService.getActiveInterpreter().catch<PythonInterpreter | undefined>(() => undefined), - interpreterService.getInterpreters(mainWorkspaceUri).catch<PythonInterpreter[]>(() => []) - ]); - const workspaceFolderCount = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders!.length : 0; - const pythonVersion = interpreter && interpreter.version ? interpreter.version.raw : undefined; - const interpreterType = interpreter ? interpreter.type : undefined; - const hasUserDefinedInterpreter = hasUserDefinedPythonPath(mainWorkspaceUri, serviceContainer); - const preferredWorkspaceInterpreter = getPreferredWorkspaceInterpreter(mainWorkspaceUri, serviceContainer); - const isAutoSelectedWorkspaceInterpreterUsed = preferredWorkspaceInterpreter ? settings.pythonPath === getPreferredWorkspaceInterpreter(mainWorkspaceUri, serviceContainer) : undefined; - const hasPython3 = interpreters - .filter(item => item && item.version ? item.version.major === 3 : false) - .length > 0; - - const props = { - condaVersion, terminal: terminalShellType, pythonVersion, interpreterType, workspaceFolderCount, hasPython3, - hasUserDefinedInterpreter, isAutoSelectedWorkspaceInterpreterUsed - }; - sendTelemetryEvent(EDITOR_LOAD, durations, props); + let appShell: IAppShell = (window as any) as IAppShell; + if (activatedServiceContainer) { + appShell = (activatedServiceContainer.get<IApplicationShell>(IApplicationShell) as any) as IAppShell; + } + appShell.showErrorMessage(msg).ignoreErrors(); } catch (ex) { - logger.logError('sendStartupTelemetry failed.', ex); + traceError('Failed to Notify User', ex); } } -function hasUserDefinedPythonPath(resource: Resource, serviceContainer: IServiceContainer) { - const workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService); - const settings = workspaceService.getConfiguration('python', resource)!.inspect<string>('pyhontPath')!; - return (settings.workspaceFolderValue && settings.workspaceFolderValue !== 'python') || - (settings.workspaceValue && settings.workspaceValue !== 'python') || - (settings.globalValue && settings.globalValue !== 'python'); -} -function getPreferredWorkspaceInterpreter(resource: Resource, serviceContainer: IServiceContainer) { - const workspaceInterpreterSelector = serviceContainer.get<IInterpreterAutoSelectionRule>(IInterpreterAutoSelectionRule, AutoSelectionRule.workspaceVirtualEnvs); - const interpreter = workspaceInterpreterSelector.getPreviouslyAutoSelectedInterpreter(resource); - return interpreter ? interpreter.path : undefined; -} diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts new file mode 100644 index 000000000000..57bcb8237eeb --- /dev/null +++ b/src/client/extensionActivation.ts @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { DebugConfigurationProvider, debug, languages, window } from 'vscode'; + +import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry'; +import { IExtensionActivationManager } from './activation/types'; +import { registerTypes as appRegisterTypes } from './application/serviceRegistry'; +import { IApplicationDiagnostics } from './application/types'; +import { IApplicationEnvironment, ICommandManager, IWorkspaceService } from './common/application/types'; +import { Commands, PYTHON_LANGUAGE, UseProposedApi } from './common/constants'; +import { registerTypes as installerRegisterTypes } from './common/installer/serviceRegistry'; +import { IFileSystem } from './common/platform/types'; +import { IConfigurationService, IDisposableRegistry, IExtensions, ILogOutputChannel, IPathUtils } from './common/types'; +import { noop } from './common/utils/misc'; +import { registerTypes as debugConfigurationRegisterTypes } from './debugger/extension/serviceRegistry'; +import { IDebugConfigurationService } from './debugger/extension/types'; +import { IInterpreterService } from './interpreter/contracts'; +import { getLanguageConfiguration } from './language/languageConfiguration'; +import { ReplProvider } from './providers/replProvider'; +import { registerTypes as providersRegisterTypes } from './providers/serviceRegistry'; +import { TerminalProvider } from './providers/terminalProvider'; +import { setExtensionInstallTelemetryProperties } from './telemetry/extensionInstallTelemetry'; +import { registerTypes as tensorBoardRegisterTypes } from './tensorBoard/serviceRegistry'; +import { registerTypes as commonRegisterTerminalTypes } from './terminals/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( + // `ext` is passed to any extra activation funcs. + ext: ExtensionState, + components: Components, + startupStopWatch: StopWatch, +): Promise<ActivationResult[]> { + // 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<ActivationResult>[] = [ + // More component activations will go here + pythonEnvironments.activate(components.pythonEnvs, ext), + ]; + return Promise.all([legacyActivationResult, ...promises]); +} + +export function activateFeatures(ext: ExtensionState, _components: Components): void { + const interpreterQuickPick: IInterpreterQuickPick = ext.legacyIOC.serviceContainer.get<IInterpreterQuickPick>( + IInterpreterQuickPick, + ); + const interpreterService: IInterpreterService = ext.legacyIOC.serviceContainer.get<IInterpreterService>( + IInterpreterService, + ); + const pathUtils = ext.legacyIOC.serviceContainer.get<IPathUtils>(IPathUtils); + registerPixiFeatures(ext.disposables); + registerAllCreateEnvironmentFeatures( + ext.disposables, + interpreterQuickPick, + ext.legacyIOC.serviceContainer.get<IPythonPathUpdaterServiceManager>(IPythonPathUpdaterServiceManager), + interpreterService, + pathUtils, + ); + const executionHelper = ext.legacyIOC.serviceContainer.get<ICodeExecutionHelper>(ICodeExecutionHelper); + const commandManager = ext.legacyIOC.serviceContainer.get<ICommandManager>(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 + +// 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(ext: ExtensionState, startupStopWatch: StopWatch): Promise<ActivationResult> { + const { legacyIOC } = ext; + const { serviceManager, serviceContainer } = legacyIOC; + + // register "services" + + // We need to setup this property before any telemetry is sent + const fs = serviceManager.get<IFileSystem>(IFileSystem); + await setExtensionInstallTelemetryProperties(fs); + + const applicationEnv = serviceManager.get<IApplicationEnvironment>(IApplicationEnvironment); + const { enableProposedApi } = applicationEnv.packageJson; + serviceManager.addSingletonInstance<boolean>(UseProposedApi, enableProposedApi); + // Feature specific registrations. + installerRegisterTypes(serviceManager); + commonRegisterTerminalTypes(serviceManager); + debugConfigurationRegisterTypes(serviceManager); + tensorBoardRegisterTypes(serviceManager); + + const extensions = serviceContainer.get<IExtensions>(IExtensions); + await setDefaultLanguageServer(extensions, serviceManager); + + // Settings are dependent on Experiment service, so we need to initialize it after experiments are activated. + serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings().register(); + + // Language feature registrations. + appRegisterTypes(serviceManager); + providersRegisterTypes(serviceManager); + activationRegisterTypes(serviceManager); + + // "initialize" "services" + + const disposables = serviceManager.get<IDisposableRegistry>(IDisposableRegistry); + const workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService); + const cmdManager = serviceContainer.get<ICommandManager>(ICommandManager); + + languages.setLanguageConfiguration(PYTHON_LANGUAGE, getLanguageConfiguration()); + if (workspaceService.isTrusted) { + const interpreterManager = serviceContainer.get<IInterpreterService>(IInterpreterService); + interpreterManager.initialize(); + if (!workspaceService.isVirtualWorkspace) { + const handlers = serviceManager.getAll<IDebugSessionEventHandlers>(IDebugSessionEventHandlers); + const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); + dispatcher.registerEventHandlers(); + const outputChannel = serviceManager.get<ILogOutputChannel>(ILogOutputChannel); + disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); + cmdManager.executeCommand('setContext', 'python.vscode.channel', applicationEnv.channel).then(noop, noop); + + serviceContainer.get<IApplicationDiagnostics>(IApplicationDiagnostics).register(); + + serviceManager.get<ITerminalAutoActivation>(ITerminalAutoActivation).register(); + + await registerPythonStartup(ext.context); + + serviceManager.get<ICodeExecutionManager>(ICodeExecutionManager).registerCommands(); + + disposables.push(new ReplProvider(serviceContainer)); + + const terminalProvider = new TerminalProvider(serviceContainer); + terminalProvider.initialize(window.activeTerminal).ignoreErrors(); + + serviceContainer + .getAll<DebugConfigurationProvider>(IDebugConfigurationService) + .forEach((debugConfigProvider) => { + disposables.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); + }); + disposables.push(terminalProvider); + + registerCreateEnvironmentTriggers(disposables); + initializePersistentStateForTriggers(ext.context); + } + } + + // "activate" everything else + + const manager = serviceContainer.get<IExtensionActivationManager>(IExtensionActivationManager); + disposables.push(manager); + + const activationPromise = manager.activate(startupStopWatch); + + return { fullyReady: activationPromise }; +} diff --git a/src/client/extensionInit.ts b/src/client/extensionInit.ts new file mode 100644 index 000000000000..b161643d2d97 --- /dev/null +++ b/src/client/extensionInit.ts @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Container } from 'inversify'; +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 * 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( + // 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>(IServiceContainer, serviceContainer); + serviceManager.addSingletonInstance<IServiceManager>(IServiceManager, serviceManager); + + serviceManager.addSingletonInstance<Disposable[]>(IDisposableRegistry, disposables); + serviceManager.addSingletonInstance<Memento>(IMemento, context.globalState, GLOBAL_MEMENTO); + serviceManager.addSingletonInstance<Memento>(IMemento, context.workspaceState, WORKSPACE_MEMENTO); + serviceManager.addSingletonInstance<IExtensionContext>(IExtensionContext, context); + + const standardOutputChannel = window.createOutputChannel(OutputChannelNames.python, { log: true }); + disposables.push(standardOutputChannel); + disposables.push(registerLogger(new OutputChannelLogger(standardOutputChannel))); + + serviceManager.addSingletonInstance<ILogOutputChannel>(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(). +} + +/** + * 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<Components> { + 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 15ef14a17859..000000000000 --- a/src/client/formatters/autoPep8Formatter.ts +++ /dev/null @@ -1,30 +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 { FORMAT } 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<vscode.TextEdit[]> { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer.get<IConfigurationService>(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(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 7fd32abae119..000000000000 --- a/src/client/formatters/baseFormatter.ts +++ /dev/null @@ -1,119 +0,0 @@ -import * as fs from 'fs-extra'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IWorkspaceService } from '../common/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; -import '../common/extensions'; -import { isNotInstalledError } from '../common/helpers'; -import { IPythonToolExecutionService } from '../common/process/types'; -import { IInstaller, IOutputChannel, Product } from '../common/types'; -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<vscode.OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.helper = serviceContainer.get<IFormatterHelper>(IFormatterHelper); - this.workspace = serviceContainer.get<IWorkspaceService>(IWorkspaceService); - } - - public abstract formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable<vscode.TextEdit[]>; - 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<vscode.TextEdit[]> { - 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. - 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>(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; - }); - vscode.window.setStatusBarMessage(`Formatting with ${this.Id}`, promise); - 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>(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 => console.error('Python Extension: promptToInstall', ex)); - } - } - - this.outputChannel.appendLine(`\n${customError}\n${error}`); - } - - private async createTempFile(document: vscode.TextDocument): Promise<string> { - return document.isDirty - ? getTempFileWithDocumentContents(document) - : document.fileName; - } - - private deleteTempFile(originalFile: string, tempFile: string): Promise<void> { - if (originalFile !== tempFile) { - return fs.unlink(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 e33a00a35e20..000000000000 --- a/src/client/formatters/blackFormatter.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -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 { FORMAT } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class BlackFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('black', Product.black, serviceContainer); - } - - public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable<vscode.TextEdit[]> { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer.get<IConfigurationService>(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 errorMessage = async () => { - // Black does not support partial formatting on purpose. - await vscode.window.showErrorMessage('Black does not support the "Format Selection" command'); - return [] as vscode.TextEdit[]; - }; - - return errorMessage(); - } - - const blackArgs = ['--diff', '--quiet']; - const promise = super.provideDocumentFormattingEdits(document, options, token, blackArgs); - sendTelemetryWhenDone(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 b4fe65547d60..000000000000 --- a/src/client/formatters/dummyFormatter.ts +++ /dev/null @@ -1,14 +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<vscode.TextEdit[]> { - return Promise.resolve([]); - } -} diff --git a/src/client/formatters/helper.ts b/src/client/formatters/helper.ts deleted file mode 100644 index 95383e69b538..000000000000 --- a/src/client/formatters/helper.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 * 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>(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 867a115c1a64..000000000000 --- a/src/client/formatters/lineFormatter.ts +++ /dev/null @@ -1,441 +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<IToken> = new TextRangeCollection<IToken>([]); - 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<IToken>, 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<IToken>, 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<IToken> | 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>(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 301bf4a9da87..000000000000 --- a/src/client/formatters/yapfFormatter.ts +++ /dev/null @@ -1,32 +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 { FORMAT } 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<vscode.TextEdit[]> { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer.get<IConfigurationService>(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(FORMAT, promise, stopWatch, { tool: 'yapf', hasCustomArgs, formatSelection }); - return promise; - } -} diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts new file mode 100644 index 000000000000..f47575cad60b --- /dev/null +++ b/src/client/interpreter/activation/service.ts @@ -0,0 +1,405 @@ +/* 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 { IPlatformService } from '../../common/platform/types'; +import * as internalScripts from '../../common/process/internal/scripts'; +import { ExecutionResult, IProcessServiceFactory } from '../../common/process/types'; +import { ITerminalHelper, TerminalShellType } from '../../common/terminal/types'; +import { ICurrentProcess, IDisposable, Resource } from '../../common/types'; +import { sleep } from '../../common/utils/async'; +import { InMemoryCache } from '../../common/utils/cacheUtils'; +import { OSType } from '../../common/utils/platform'; +import { 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 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. +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, +}; + +const condaRetryMessages = [ + 'The process cannot access the file because it is being used by another process', + 'The directory is not empty', +]; + +/** + * This class exists so that the environment variable fetching can be cached in between tests. Normally + * this cache resides in memory for the duration of the EnvironmentActivationService's lifetime, but in the case + * of our functional tests, we want the cached data to exist outside of each test (where each test will destroy the EnvironmentActivationService) + * This gives each test a 3 or 4 second speedup. + */ +export class EnvironmentActivationServiceCache { + private static useStatic = false; + + private static staticMap = new Map<string, InMemoryCache<NodeJS.ProcessEnv | undefined>>(); + + private normalMap = new Map<string, InMemoryCache<NodeJS.ProcessEnv | undefined>>(); + + public static forceUseStatic(): void { + EnvironmentActivationServiceCache.useStatic = true; + } + + public static forceUseNormal(): void { + EnvironmentActivationServiceCache.useStatic = false; + } + + public get(key: string): InMemoryCache<NodeJS.ProcessEnv | undefined> | undefined { + if (EnvironmentActivationServiceCache.useStatic) { + return EnvironmentActivationServiceCache.staticMap.get(key); + } + return this.normalMap.get(key); + } + + public set(key: string, value: InMemoryCache<NodeJS.ProcessEnv | undefined>): void { + if (EnvironmentActivationServiceCache.useStatic) { + EnvironmentActivationServiceCache.staticMap.set(key, value); + } else { + this.normalMap.set(key, value); + } + } + + public delete(key: string): void { + if (EnvironmentActivationServiceCache.useStatic) { + EnvironmentActivationServiceCache.staticMap.delete(key); + } else { + this.normalMap.delete(key); + } + } + + public clear(): void { + // Don't clear during a test as the environment isn't going to change + if (!EnvironmentActivationServiceCache.useStatic) { + this.normalMap.clear(); + } + } +} + +@injectable() +export class EnvironmentActivationService implements IEnvironmentActivationService, IDisposable { + private readonly disposables: IDisposable[] = []; + + private readonly activatedEnvVariablesCache = new EnvironmentActivationServiceCache(); + + constructor( + @inject(ITerminalHelper) private readonly helper: ITerminalHelper, + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IProcessServiceFactory) private processServiceFactory: IProcessServiceFactory, + @inject(ICurrentProcess) private currentProcess: ICurrentProcess, + @inject(IWorkspaceService) private workspace: IWorkspaceService, + @inject(IInterpreterService) private interpreterService: IInterpreterService, + @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider, + ) { + this.envVarsService.onDidEnvironmentVariablesChange( + () => this.activatedEnvVariablesCache.clear(), + this, + this.disposables, + ); + } + + public dispose(): void { + this.disposables.forEach((d) => d.dispose()); + } + + @traceDecoratorVerbose('getActivatedEnvironmentVariables', TraceOptions.Arguments) + public async getActivatedEnvironmentVariables( + resource: Resource, + interpreter?: PythonEnvironment, + allowExceptions?: boolean, + shell?: string, + ): Promise<NodeJS.ProcessEnv | undefined> { + 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}_${shell}`; + + if (this.activatedEnvVariablesCache.get(cacheKey)?.hasData) { + return this.activatedEnvVariablesCache.get(cacheKey)!.data; + } + + // Cache only if successful, else keep trying & failing if necessary. + const memCache = new InMemoryCache<NodeJS.ProcessEnv | undefined>(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<EnvironmentVariables> { + // 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<string[] | undefined> { + const shellInfo = defaultShells[this.platform.osType]; + if (!shellInfo) { + return []; + } + return this.helper.getEnvironmentActivationShellCommands(resource, shellInfo.shellType, interpreter); + } + + public async getActivatedEnvironmentVariablesImpl( + resource: Resource, + interpreter?: PythonEnvironment, + allowExceptions?: boolean, + shell?: string, + ): Promise<NodeJS.ProcessEnv | undefined> { + let shellInfo = defaultShells[this.platform.osType]; + if (!shellInfo) { + return undefined; + } + if (shell) { + const customShellType = identifyShellFromShellPath(shell); + shellInfo = { shellType: customShellType, shell }; + } + try { + const processService = await this.processServiceFactory.create(resource); + const customEnvVars = (await this.envVarsService.getEnvironmentVariables(resource)) ?? {}; + const hasCustomEnvVars = Object.keys(customEnvVars).length; + const env = hasCustomEnvVars ? customEnvVars : { ...this.currentProcess.env }; + + 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(`Activating Environment to capture Environment variables, ${command}`); + + // Do some wrapping of the call. For two reasons: + // 1) Conda activate can hang on certain systems. Fail after 30 seconds. + // See the discussion from hidesoon in this issue: https://github.com/Microsoft/vscode-python/issues/4424 + // His issue is conda never finishing during activate. This is a conda issue, but we + // should at least tell the user. + // 2) Retry because of this issue here: https://github.com/microsoft/vscode-python/issues/9244 + // This happens on AzDo machines a bunch when using Conda (and we can't dictate the conda version in order to get the fix) + let result: ExecutionResult<string> | undefined; + let tryCount = 1; + let returnedEnv: NodeJS.ProcessEnv | undefined; + while (!result) { + try { + result = await processService.shellExec(command, { + env, + shell: shellInfo.shell, + timeout: + interpreter?.envType === EnvironmentType.Conda + ? CONDA_ENVIRONMENT_TIMEOUT + : ENVIRONMENT_TIMEOUT, + maxBuffer: 1000 * 1000, + throwOnStdErr: false, + }); + + 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 as Error).toString(); + if (condaRetryMessages.find((m) => excString.includes(m)) && tryCount < 10) { + traceInfo(`Conda is busy, attempting to retry ...`); + result = undefined; + tryCount += 1; + await sleep(500); + } else { + throw exc; + } + } + } + + // Put back the PYTHONWARNINGS value + if (oldWarnings && returnedEnv) { + returnedEnv[PYTHON_WARNINGS] = oldWarnings; + } else if (returnedEnv) { + delete returnedEnv[PYTHON_WARNINGS]; + } + return returnedEnv; + } catch (e) { + traceError('getActivatedEnvironmentVariables', e); + sendTelemetryEvent(EventName.ACTIVATE_ENV_TO_GET_ENV_VARS_FAILED, undefined, { + isPossiblyCondaEnv: interpreter?.envType === EnvironmentType.Conda, + terminal: shellInfo.shellType, + }); + + // Some callers want this to bubble out, others don't + if (allowExceptions) { + throw e; + } + } + return undefined; + } + + // 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/types.ts b/src/client/interpreter/activation/types.ts new file mode 100644 index 000000000000..e00ef9b62b3f --- /dev/null +++ b/src/client/interpreter/activation/types.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Resource } from '../../common/types'; +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<EnvironmentVariables>; + getActivatedEnvironmentVariables( + resource: Resource, + interpreter?: PythonEnvironment, + allowExceptions?: boolean, + shell?: string, + ): Promise<NodeJS.ProcessEnv | undefined>; + getEnvironmentActivationShellCommands( + resource: Resource, + interpreter?: PythonEnvironment, + ): Promise<string[] | undefined>; +} diff --git a/src/client/interpreter/autoSelection/index.ts b/src/client/interpreter/autoSelection/index.ts index 43fbffa515e7..5ad5362e8210 100644 --- a/src/client/interpreter/autoSelection/index.ts +++ b/src/client/interpreter/autoSelection/index.ts @@ -3,74 +3,85 @@ '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 { IInterpreterHelper, PythonInterpreter } from '../contracts'; -import { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from './types'; +import { IExperimentService, IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; +import { createDeferred, Deferred } from '../../common/utils/async'; +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 { IInterpreterComparer } from '../configuration/types'; +import { IInterpreterHelper, IInterpreterService } from '../contracts'; +import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './types'; const preferredGlobalInterpreter = 'preferredGlobalPyInterpreter'; const workspacePathNameForGlobalWorkspaces = ''; @injectable() export class InterpreterAutoSelectionService implements IInterpreterAutoSelectionService { + protected readonly autoSelectedWorkspacePromises = new Map<string, Deferred<void>>(); + private readonly didAutoSelectedInterpreterEmitter = new EventEmitter<void>(); - private readonly autoSelectedInterpreterByWorkspace = new Map<string, PythonInterpreter | undefined>(); - private globallyPreferredInterpreter!: IPersistentState<PythonInterpreter | undefined>; - private readonly rules: IInterpreterAutoSelectionRule[] = []; - constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + + private readonly autoSelectedInterpreterByWorkspace = new Map<string, PythonEnvironment | undefined>(); + + private globallyPreferredInterpreter: IPersistentState< + PythonEnvironment | undefined + > = this.stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + 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(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper) { - - // 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]); + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IInterpreterComparer) private readonly envTypeComparer: IInterpreterComparer, + @inject(IInterpreterAutoSelectionProxyService) proxy: IInterpreterAutoSelectionProxyService, + @inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + ) { 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); } + + /** + * 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<void> { - Promise.all(this.rules.map(item => item.autoSelectInterpreter(undefined))).ignoreErrors(); - await this.initializeStore(); - await this.userDefinedInterpreter.autoSelectInterpreter(resource, this); - this.didAutoSelectedInterpreterEmitter.fire(); + const key = this.getWorkspacePathKey(resource); + const useCachedInterpreter = this.autoSelectedWorkspacePromises.has(key); + + if (!useCachedInterpreter) { + const deferred = createDeferred<void>(); + this.autoSelectedWorkspacePromises.set(key, deferred); + + await this.initializeStore(resource); + await this.clearWorkspaceStoreIfInvalid(resource); + await this.autoselectInterpreterWithLocators(resource); + + deferred.resolve(); + } + + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, undefined, { + useCachedInterpreter, + }); + + return this.autoSelectedWorkspacePromises.get(key)!.promise; } + public get onDidChangeAutoSelectedInterpreter(): Event<void> { 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. @@ -86,20 +97,36 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio return this.globallyPreferredInterpreter.value; } - public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonInterpreter | undefined) { - await this.storeAutoSelectedInterperter(resource, interpreter); + + public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonEnvironment | undefined): Promise<void> { + await this.storeAutoSelectedInterpreter(resource, interpreter); } - public async setGlobalInterpreter(interpreter: PythonInterpreter) { - await this.storeAutoSelectedInterperter(undefined, interpreter); + + public async setGlobalInterpreter(interpreter: PythonEnvironment): Promise<void> { + await this.storeAutoSelectedInterpreter(undefined, interpreter); } - protected async storeAutoSelectedInterperter(resource: Resource, interpreter: PythonInterpreter | undefined) { + + protected async clearWorkspaceStoreIfInvalid(resource: Resource): Promise<void> { + const stateStore = this.getWorkspaceState(resource); + if (stateStore && stateStore.value && !(await this.fs.fileExists(stateStore.value.path))) { + await stateStore.updateValue(undefined); + } + } + + protected async storeAutoSelectedInterpreter( + resource: Resource, + interpreter: PythonEnvironment | undefined, + ): Promise<void> { const workspaceFolderPath = this.getWorkspacePathKey(resource); if (workspaceFolderPath === workspacePathNameForGlobalWorkspaces) { // Update store only if this version is better. - if (this.globallyPreferredInterpreter.value && + if ( + this.globallyPreferredInterpreter.value && this.globallyPreferredInterpreter.value.version && - interpreter && interpreter.version && - compare(this.globallyPreferredInterpreter.value.version.raw, interpreter.version.raw) > 0) { + interpreter && + interpreter.version && + compareSemVerLikeVersions(this.globallyPreferredInterpreter.value.version, interpreter.version) > 0 + ) { return; } @@ -113,31 +140,119 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio } this.autoSelectedInterpreterByWorkspace.set(workspaceFolderPath, interpreter); } - - this.didAutoSelectedInterpreterEmitter.fire(); } - protected async initializeStore() { + + protected async initializeStore(resource: Resource): Promise<void> { + const workspaceFolderPath = this.getWorkspacePathKey(resource); + // Since we're initializing for this resource, + // Ensure any cached information for this workspace have been removed. + this.autoSelectedInterpreterByWorkspace.delete(workspaceFolderPath); if (this.globallyPreferredInterpreter) { return; } await this.clearStoreIfFileIsInvalid(); } + private async clearStoreIfFileIsInvalid() { - this.globallyPreferredInterpreter = this.stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(preferredGlobalInterpreter, undefined); - if (this.globallyPreferredInterpreter.value && !await this.fs.fileExists(this.globallyPreferredInterpreter.value.path)) { + this.globallyPreferredInterpreter = this.stateFactory.createGlobalPersistentState< + PythonEnvironment | undefined + >(preferredGlobalInterpreter, undefined); + if ( + this.globallyPreferredInterpreter.value && + !(await this.fs.fileExists(this.globallyPreferredInterpreter.value.path)) + ) { await this.globallyPreferredInterpreter.updateValue(undefined); } } + private getWorkspacePathKey(resource: Resource): string { - const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; - return workspaceFolder ? workspaceFolder.uri.fsPath : workspacePathNameForGlobalWorkspaces; + return this.workspaceService.getWorkspaceFolderIdentifier(resource, workspacePathNameForGlobalWorkspaces); } - private getWorkspaceState(resource: Resource): undefined | IPersistentState<PythonInterpreter | undefined> { + + private getWorkspaceState(resource: Resource): undefined | IPersistentState<PythonEnvironment | undefined> { 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<boolean | undefined> { + const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + const key = `autoSelectionInterpretersQueried-${workspaceUri?.folderUri.fsPath || 'global'}`; return this.stateFactory.createWorkspacePersistentState(key, undefined); } + + private getAutoSelectionQueriedOnceState(): IPersistentState<boolean | undefined> { + 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<void> { + // 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/proxy.ts b/src/client/interpreter/autoSelection/proxy.ts index fae3bd443c4f..ea9be593d386 100644 --- a/src/client/interpreter/autoSelection/proxy.ts +++ b/src/client/interpreter/autoSelection/proxy.ts @@ -5,26 +5,34 @@ import { inject, injectable } from 'inversify'; import { Event, EventEmitter, Uri } from 'vscode'; -import { IAsyncDisposableRegistry, IDisposableRegistry, Resource } from '../../common/types'; -import { PythonInterpreter } from '../contracts'; -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<void>(); - 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.disposables.push( + this.instance.onDidChangeAutoSelectedInterpreter(() => this.didAutoSelectedInterpreterEmitter.fire()), + ); } + public get onDidChangeAutoSelectedInterpreter(): Event<void> { 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<void>{ + + public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonEnvironment | undefined): Promise<void> { 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 e4fa897dd19a..000000000000 --- a/src/client/interpreter/autoSelection/rules/baseRule.ts +++ /dev/null @@ -1,82 +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 { IFileSystem } from '../../../common/platform/types'; -import { IPersistentState, IPersistentStateFactory, Resource } from '../../../common/types'; -import { StopWatch } from '../../../common/utils/stopWatch'; -import { sendTelemetryEvent } from '../../../telemetry'; -import { PYTHON_INTERPRETER_AUTO_SELECTION } from '../../../telemetry/constants'; -import { PythonInterpreter } from '../../contracts'; -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<PythonInterpreter | undefined>; - constructor(@unmanaged() private readonly ruleName: AutoSelectionRule, - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory) { - this.stateStore = stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(`InterpreterAutoSeletionRule-${this.ruleName}`, undefined); - } - public setNextRule(rule: IInterpreterAutoSelectionRule): void { - this.nextRule = rule; - } - public async autoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise<void> { - await this.clearCachedInterpreterIfInvalid(resource); - const stopWatch = new StopWatch(); - const action = await this.onAutoSelectInterpreter(resource, manager); - const identified = action === NextAction.runNextRule; - sendTelemetryEvent(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 { - return this.stateStore.value; - } - protected abstract onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise<NextAction>; - protected async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise<boolean> { - 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(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(PYTHON_INTERPRETER_AUTO_SELECTION, {}, { rule: this.ruleName, updated }); - await this.stateStore.updateValue(interpreter); - } - protected async next(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise<void> { - 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 cbc3fe1ef01b..000000000000 --- a/src/client/interpreter/autoSelection/rules/cached.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -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<NextAction> { - const cachedInterpreters = this.rules - .map(item => item.getPreviouslyAutoSelectedInterpreter(resource)) - .filter(item => !!item) - .map(item => item!); - const bestInterpreter = this.helper.getBestInterpreter(cachedInterpreters); - 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 b277cfe9ab31..000000000000 --- a/src/client/interpreter/autoSelection/rules/currentPath.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 { 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<NextAction> { - const interpreters = await this.currentPathInterpreterLocator.getInterpreters(resource); - const bestInterpreter = this.helper.getBestInterpreter(interpreters); - 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 793cbd128009..000000000000 --- a/src/client/interpreter/autoSelection/rules/settings.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 { IWorkspaceService } from '../../../common/application/types'; -import { IFileSystem } from '../../../common/platform/types'; -import { 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) { - - super(AutoSelectionRule.settings, fs, stateFactory); - } - protected async onAutoSelectInterpreter(_resource: Resource, _manager?: IInterpreterAutoSelectionService): Promise<NextAction> { - // tslint:disable-next-line:no-any - const pythonConfig = this.workspaceService.getConfiguration('python', null as any)!; - const pythonPathInConfig = pythonConfig.inspect<string>('pythonPath')!; - // 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 15bee26fd16d..000000000000 --- a/src/client/interpreter/autoSelection/rules/system.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 { IFileSystem } from '../../../common/platform/types'; -import { IPersistentStateFactory, Resource } from '../../../common/types'; -import { IInterpreterHelper, IInterpreterService, InterpreterType } 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<NextAction> { - 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); - 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 e043f751539a..000000000000 --- a/src/client/interpreter/autoSelection/rules/winRegistry.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -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<NextAction> { - if (this.platform.osType !== OSType.Windows) { - return NextAction.runNextRule; - } - const interpreters = await this.winRegInterpreterLocator.getInterpreters(resource); - const bestInterpreter = this.helper.getBestInterpreter(interpreters); - 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 6a4230391dad..000000000000 --- a/src/client/interpreter/autoSelection/rules/workspaceEnv.ts +++ /dev/null @@ -1,95 +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 { IFileSystem, IPlatformService } from '../../../common/platform/types'; -import { IPersistentStateFactory, Resource } from '../../../common/types'; -import { createDeferredFromPromise } from '../../../common/utils/async'; -import { OSType } from '../../../common/utils/platform'; -import { IPythonPathUpdaterServiceManager } from '../../configuration/types'; -import { IInterpreterHelper, IInterpreterLocatorService, PIPENV_SERVICE, PythonInterpreter, 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(IPythonPathUpdaterServiceManager) private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager, - @inject(IInterpreterLocatorService) @named(PIPENV_SERVICE) private readonly pipEnvInterpreterLocator: IInterpreterLocatorService, - @inject(IInterpreterLocatorService) @named(WORKSPACE_VIRTUAL_ENV_SERVICE) private readonly workspaceVirtualEnvInterpreterLocator: IInterpreterLocatorService) { - - super(AutoSelectionRule.workspaceVirtualEnvs, fs, stateFactory); - } - protected async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise<NextAction> { - const workspacePath = this.helper.getActiveWorkspaceUri(resource); - if (!workspacePath) { - return NextAction.runNextRule; - } - - const pythonConfig = this.workspaceService.getConfiguration('python', workspacePath.folderUri)!; - const pythonPathInConfig = pythonConfig.inspect<string>('pythonPath')!; - // If user has defined custom values in settings for this workspace folder, then use that. - if (pythonPathInConfig.workspaceFolderValue) { - return NextAction.runNextRule; - } - const pipEnvPromise = createDeferredFromPromise(this.pipEnvInterpreterLocator.getInterpreters(workspacePath.folderUri)); - const virtualEnvPromise = createDeferredFromPromise(this.getWorkspaceVirtualEnvInterpreters(workspacePath.folderUri)); - - // Use only one, we currently do not have support for both pipenv and virtual env in same workspace. - // If users have this, then theu can specify which one is to be used. - const interpreters = await Promise.race([pipEnvPromise.promise, virtualEnvPromise.promise]); - let bestInterpreter: PythonInterpreter | undefined; - if (Array.isArray(interpreters) && interpreters.length > 0) { - bestInterpreter = this.helper.getBestInterpreter(interpreters); - } else { - const [pipEnv, virtualEnv] = await Promise.all([pipEnvPromise.promise, virtualEnvPromise.promise]); - const pipEnvList = Array.isArray(pipEnv) ? pipEnv : []; - const virtualEnvList = Array.isArray(virtualEnv) ? virtualEnv : []; - bestInterpreter = this.helper.getBestInterpreter(pipEnvList.concat(virtualEnvList)); - } - if (bestInterpreter && manager) { - await this.cacheSelectedInterpreter(workspacePath.folderUri, bestInterpreter); - await manager.setWorkspaceInterpreter(workspacePath.folderUri!, bestInterpreter); - } - return NextAction.runNextRule; - } - protected async getWorkspaceVirtualEnvInterpreters(resource: Resource): Promise<PythonInterpreter[] | undefined> { - 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, 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); - }); - } - protected async cacheSelectedInterpreter(resource: Resource, interpreter: PythonInterpreter | undefined) { - // We should never clear settings in user settings.json. - if (!interpreter) { - return; - } - const activeWorkspace = this.helper.getActiveWorkspaceUri(resource); - if (!activeWorkspace) { - return; - } - await this.pythonPathUpdaterService.updatePythonPath(interpreter.path, activeWorkspace.configTarget, 'load', activeWorkspace.folderUri); - await super.cacheSelectedInterpreter(resource, interpreter); - } -} diff --git a/src/client/interpreter/autoSelection/types.ts b/src/client/interpreter/autoSelection/types.ts index 4cd37b359358..91d0224717d4 100644 --- a/src/client/interpreter/autoSelection/types.ts +++ b/src/client/interpreter/autoSelection/types.ts @@ -5,45 +5,37 @@ import { Event, Uri } from 'vscode'; import { Resource } from '../../common/types'; -import { PythonInterpreter } from '../contracts'; +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<void>; - getAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined; - registerInstance?(instance: IInterpreterAutoSeletionProxyService): void; - setWorkspaceInterpreter(resource: Uri, interpreter: PythonInterpreter | undefined): Promise<void>; + getAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined; + registerInstance?(instance: IInterpreterAutoSelectionProxyService): void; + setWorkspaceInterpreter(resource: Uri, interpreter: PythonEnvironment | undefined): Promise<void>; } export const IInterpreterAutoSelectionService = Symbol('IInterpreterAutoSelectionService'); -export interface IInterpreterAutoSelectionService extends IInterpreterAutoSeletionProxyService { +export interface IInterpreterAutoSelectionService extends IInterpreterAutoSelectionProxyService { readonly onDidChangeAutoSelectedInterpreter: Event<void>; autoSelectInterpreter(resource: Resource): Promise<void>; - setGlobalInterpreter(interpreter: PythonInterpreter | undefined): Promise<void>; + getAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined; + setGlobalInterpreter(interpreter: PythonEnvironment | undefined): Promise<void>; } export enum AutoSelectionRule { + all = 'all', currentPath = 'currentPath', workspaceVirtualEnvs = 'workspaceEnvs', settings = 'settings', cachedInterpreters = 'cachedInterpreters', systemWide = 'system', - windowsRegistry = 'windowsRegistry' -} - -export const IInterpreterAutoSelectionRule = Symbol('IInterpreterAutoSelectionRule'); -export interface IInterpreterAutoSelectionRule { - setNextRule(rule: IInterpreterAutoSelectionRule): void; - autoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise<void>; - getPreviouslyAutoSelectedInterpreter(resource: Resource): PythonInterpreter | 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<string, string | undefined>(); + + 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<void> { + 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 9d0c1cdf6912..000000000000 --- a/src/client/interpreter/configuration/interpreterComparer.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 { getArchitectureDisplayName } from '../../common/platform/registry'; -import { IInterpreterHelper, PythonInterpreter } 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.ts b/src/client/interpreter/configuration/interpreterSelector.ts deleted file mode 100644 index 8afaffd63554..000000000000 --- a/src/client/interpreter/configuration/interpreterSelector.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { inject, injectable } from 'inversify'; -import { ConfigurationTarget, Disposable, QuickPickItem, QuickPickOptions, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types'; -import { Commands } from '../../common/constants'; -import { IConfigurationService, IPathUtils } from '../../common/types'; -import { IInterpreterService, IShebangCodeLensProvider, PythonInterpreter, WorkspacePythonPath } from '../contracts'; -import { IInterpreterComparer, IInterpreterSelector, IPythonPathUpdaterServiceManager } from './types'; - -export interface IInterpreterQuickPickItem extends QuickPickItem { - path: string; -} - -@injectable() -export class InterpreterSelector implements IInterpreterSelector { - private disposables: Disposable[] = []; - - constructor(@inject(IInterpreterService) private readonly interpreterManager: IInterpreterService, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, - @inject(IDocumentManager) private readonly documentManager: IDocumentManager, - @inject(IPathUtils) private readonly pathUtils: IPathUtils, - @inject(IInterpreterComparer) private readonly interpreterComparer: IInterpreterComparer, - @inject(IPythonPathUpdaterServiceManager) private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager, - @inject(IShebangCodeLensProvider) private readonly shebangCodeLensProvider: IShebangCodeLensProvider, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(ICommandManager) private readonly commandManager: ICommandManager) { - } - public dispose() { - this.disposables.forEach(disposable => disposable.dispose()); - } - - public initialize() { - this.disposables.push(this.commandManager.registerCommand(Commands.Set_Interpreter, this.setInterpreter.bind(this))); - this.disposables.push(this.commandManager.registerCommand(Commands.Set_ShebangInterpreter, this.setShebangInterpreter.bind(this))); - } - - public async getSuggestions(resourceUri?: Uri) { - const interpreters = await this.interpreterManager.getInterpreters(resourceUri); - interpreters.sort(this.interpreterComparer.compare.bind(this.interpreterComparer)); - return Promise.all(interpreters.map(item => this.suggestionToQuickPickItem(item, resourceUri))); - } - protected async suggestionToQuickPickItem(suggestion: PythonInterpreter, workspaceUri?: Uri): Promise<IInterpreterQuickPickItem> { - const detail = this.pathUtils.getDisplayName(suggestion.path, workspaceUri ? workspaceUri.fsPath : undefined); - const cachedPrefix = suggestion.cachedEntry ? '(cached) ' : ''; - return { - // tslint:disable-next-line:no-non-null-assertion - label: suggestion.displayName!, - detail: `${cachedPrefix}${detail}`, - path: suggestion.path - }; - } - - protected async setInterpreter() { - const setInterpreterGlobally = !Array.isArray(this.workspaceService.workspaceFolders) || this.workspaceService.workspaceFolders.length === 0; - let configTarget = ConfigurationTarget.Global; - let wkspace: Uri | undefined; - if (!setInterpreterGlobally) { - const targetConfig = await this.getWorkspaceToSetPythonPath(); - if (!targetConfig) { - return; - } - configTarget = targetConfig.configTarget; - wkspace = targetConfig.folderUri; - } - - const suggestions = await this.getSuggestions(wkspace); - const currentPythonPath = this.pathUtils.getDisplayName(this.configurationService.getSettings(wkspace).pythonPath, wkspace ? wkspace.fsPath : undefined); - const quickPickOptions: QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${currentPythonPath}` - }; - - const selection = await this.applicationShell.showQuickPick(suggestions, quickPickOptions); - if (selection !== undefined) { - await this.pythonPathUpdaterService.updatePythonPath(selection.path, configTarget, 'ui', wkspace); - } - } - - protected async setShebangInterpreter(): Promise<void> { - const shebang = await this.shebangCodeLensProvider.detectShebang(this.documentManager.activeTextEditor!.document); - 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); - } - private async getWorkspaceToSetPythonPath(): Promise<WorkspacePythonPath | undefined> { - if (!Array.isArray(this.workspaceService.workspaceFolders) || this.workspaceService.workspaceFolders.length === 0) { - return undefined; - } - if (this.workspaceService.workspaceFolders.length === 1) { - return { folderUri: this.workspaceService.workspaceFolders[0].uri, configTarget: ConfigurationTarget.WorkspaceFolder }; - } - - // Ok we have multiple workspaces, get the user to pick a folder. - const workspaceFolder = await this.applicationShell.showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); - return workspaceFolder ? { folderUri: workspaceFolder.uri, configTarget: ConfigurationTarget.WorkspaceFolder } : undefined; - } -} diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/base.ts b/src/client/interpreter/configuration/interpreterSelector/commands/base.ts new file mode 100644 index 000000000000..6307e286dbfe --- /dev/null +++ b/src/client/interpreter/configuration/interpreterSelector/commands/base.ts @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { injectable, unmanaged } from 'inversify'; +import * as path from 'path'; +import { ConfigurationTarget, Disposable, QuickPickItem, Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../../../activation/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../../common/application/types'; +import { 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 pathUtils: IPathUtils, + @unmanaged() protected readonly configurationService: IConfigurationService, + ) { + this.disposables.push(this); + } + + public dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + } + + public abstract activate(): Promise<void>; + + protected async getConfigTargets(options?: { + resetTarget?: boolean; + }): Promise< + | { + folderUri: Resource; + configTarget: ConfigurationTarget; + }[] + | undefined + > { + const workspaceFolders = this.workspaceService.workspaceFolders; + if (workspaceFolders === undefined || workspaceFolders.length === 0) { + return [ + { + folderUri: undefined, + configTarget: ConfigurationTarget.Global, + }, + ]; + } + if (workspaceFolders.length === 1) { + return [ + { + folderUri: workspaceFolders[0].uri, + configTarget: ConfigurationTarget.WorkspaceFolder, + }, + ]; + } + + // Ok we have multiple workspaces, get the user to pick a folder. + + 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: options?.resetTarget ? Interpreters.clearAtWorkspace : Interpreters.entireWorkspace, + uri: workspaceFolders[0].uri, + }, + ); + + const selection = await this.applicationShell.showQuickPick(quickPickItems, { + 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 || 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<void> { + this.disposables.push(this.commandManager.registerCommand(Commands.InstallPython, () => this._installPython())); + } + + public async _installPython(): Promise<void> { + 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<PackageManagers, string[]> = { + 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<void> { + 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<void> { + 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 new file mode 100644 index 000000000000..c10f90781adb --- /dev/null +++ b/src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { 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 { + constructor( + @inject(IPythonPathUpdaterServiceManager) pythonPathUpdaterService: IPythonPathUpdaterServiceManager, + @inject(ICommandManager) commandManager: ICommandManager, + @inject(IApplicationShell) applicationShell: IApplicationShell, + @inject(IWorkspaceService) workspaceService: IWorkspaceService, + @inject(IPathUtils) pathUtils: IPathUtils, + @inject(IConfigurationService) configurationService: IConfigurationService, + ) { + super( + pythonPathUpdaterService, + commandManager, + applicationShell, + workspaceService, + pathUtils, + configurationService, + ); + } + + public async activate() { + this.disposables.push( + this.commandManager.registerCommand(Commands.ClearWorkspaceInterpreter, this.resetInterpreter.bind(this)), + ); + } + + public async resetInterpreter() { + const targetConfigs = await this.getConfigTargets({ resetTarget: true }); + if (!targetConfigs) { + return; + } + 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 new file mode 100644 index 000000000000..a629d1bc793c --- /dev/null +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -0,0 +1,723 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +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, 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 { Common, InterpreterQuickPickList } from '../../../../common/utils/localize'; +import { noop } from '../../../../common/utils/misc'; +import { + IMultiStepInput, + IMultiStepInputFactory, + InputFlowAction, + InputStep, + 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 { 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 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) pathUtils: IPathUtils, + @inject(IPythonPathUpdaterServiceManager) + pythonPathUpdaterService: IPythonPathUpdaterServiceManager, + @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(IInterpreterService) private readonly interpreterService: IInterpreterService, + ) { + super( + pythonPathUpdaterService, + commandManager, + applicationShell, + workspaceService, + pathUtils, + configurationService, + ); + } + + public async activate(): Promise<void> { + this.disposables.push( + this.commandManager.registerCommand(Commands.Set_Interpreter, this.setInterpreter.bind(this)), + ); + } + + public async _pickInterpreter( + input: IMultiStepInput<InterpreterStateArgs>, + state: InterpreterStateArgs, + filter?: (i: PythonEnvironment) => boolean, + params?: InterpreterQuickPickParams, + ): Promise<void | InputStep<InterpreterStateArgs>> { + // 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, + ); + 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. + }, + }); + } + + const selection = await input.showQuickPick<QuickPickType, IQuickPickParameters<QuickPickType>>({ + placeholder, + items: suggestions, + 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, + 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) { + 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<QuickPickType>) { + 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<string>('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<QuickPickType>, + 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<QuickPickItem>, + 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<InterpreterStateArgs>, + state: InterpreterStateArgs, + ): Promise<void | InputStep<InterpreterStateArgs>> { + const items: QuickPickItem[] = [ + { + label: InterpreterQuickPickList.browsePath.label, + detail: InterpreterQuickPickList.browsePath.detail, + }, + ]; + + const selection = await input.showQuickPick({ + placeholder: InterpreterQuickPickList.enterPath.placeholder, + items, + acceptFilterBoxTextAsSelection: true, + }); + + if (typeof selection === 'string') { + // User entered text in the filter box to enter path to python, store it + sendTelemetryEvent(EventName.SELECT_INTERPRETER_ENTER_CHOICE, undefined, { choice: 'enter' }); + state.path = selection; + 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, + canSelectMany: false, + 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(options?: { + hideCreateVenv?: boolean; + showBackButton?: boolean; + }): Promise<SelectEnvironmentResult | undefined> { + const targetConfig = await this.getConfigTargets(); + if (!targetConfig) { + return; + } + const { configTarget } = targetConfig[0]; + const wkspace = targetConfig[0].folderUri; + const interpreterState: InterpreterStateArgs = { path: undefined, workspace: wkspace }; + const multiStep = this.multiStepFactory.create<InterpreterStateArgs>(); + 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<string | undefined> { + const interpreterState: InterpreterStateArgs = { path: undefined, workspace }; + const multiStep = this.multiStepFactory.create<InterpreterStateArgs>(); + 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/interpreterSelector.ts b/src/client/interpreter/configuration/interpreterSelector/interpreterSelector.ts new file mode 100644 index 000000000000..6b33245bb907 --- /dev/null +++ b/src/client/interpreter/configuration/interpreterSelector/interpreterSelector.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Disposable, Uri } from 'vscode'; +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'; + +@injectable() +export class InterpreterSelector implements IInterpreterSelector { + private disposables: Disposable[] = []; + + constructor( + @inject(IInterpreterService) private readonly interpreterManager: IInterpreterService, + @inject(IInterpreterComparer) private readonly envTypeComparer: IInterpreterComparer, + @inject(IPathUtils) private readonly pathUtils: IPathUtils, + ) {} + + public dispose(): void { + this.disposables.forEach((disposable) => disposable.dispose()); + } + + 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<IInterpreterQuickPickItem[]> { + 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))); + } + + 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 { + 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 13f5210d9c93..9814ff6ee4cb 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterService.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterService.ts @@ -1,63 +1,76 @@ import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { ConfigurationTarget, Uri, window } from 'vscode'; -import { InterpreterInfomation, IPythonExecutionFactory } from '../../common/process/types'; +import { ConfigurationTarget, l10n, Uri, window } from 'vscode'; import { StopWatch } from '../../common/utils/stopWatch'; -import { IServiceContainer } from '../../ioc/types'; +import { SystemVariables } from '../../common/variables/systemVariables'; +import { traceError } from '../../logging'; import { sendTelemetryEvent } from '../../telemetry'; -import { PYTHON_INTERPRETER } from '../../telemetry/constants'; +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>(IPythonPathUpdaterServiceFactory); - this.interpreterVersionService = serviceContainer.get<IInterpreterVersionService>(IInterpreterVersionService); - this.executionFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); - } - public async updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', wkspace?: Uri): Promise<void> { + 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, + ): Promise<void> { const stopWatch = new StopWatch(); const pythonPathUpdater = this.getPythonUpdaterService(configTarget, wkspace); let failed = false; try { - await pythonPathUpdater.updatePythonPath(path.normalize(pythonPath)); - } 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 message = reason && typeof reason.message === 'string' ? reason.message as string : ''; - window.showErrorMessage(`Failed to set 'pythonPath'. Error: ${message}`); - console.error(reason); + const reason = err as Error; + const message = reason && typeof reason.message === 'string' ? (reason.message as string) : ''; + 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 => console.error('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) { - const telemtryProperties: PythonInterpreterTelemetry = { - failed, trigger + + private async sendTelemetry( + duration: number, + failed: boolean, + trigger: 'ui' | 'shebang' | 'load', + pythonPath: string | undefined, + wkspace?: Uri, + ) { + const telemetryProperties: PythonInterpreterTelemetry = { + failed, + trigger, }; - if (!failed) { - const processService = await this.executionFactory.create({ pythonPath }); - const infoPromise = processService.getInterpreterInformation() - .catch<InterpreterInfomation | undefined>(() => undefined); - const pipVersionPromise = this.interpreterVersionService.getPipVersion(pythonPath) - .then(value => value.length === 0 ? undefined : value) - .catch<string>(() => ''); - const [info, pipVersion] = await Promise.all([infoPromise, pipVersionPromise]); - if (info && info.version) { - telemtryProperties.pythonVersion = info.version.raw; - } - if (pipVersion) { - telemtryProperties.pipVersion = pipVersion; + if (!failed && pythonPath) { + const systemVariables = new SystemVariables(undefined, wkspace?.fsPath); + const interpreterInfo = await this.pyenvs.getInterpreterInformation(systemVariables.resolveAny(pythonPath)); + if (interpreterInfo) { + telemetryProperties.pythonVersion = interpreterInfo.version?.raw; } } - sendTelemetryEvent(PYTHON_INTERPRETER, duration, telemtryProperties); + + sendTelemetryEvent(EventName.PYTHON_INTERPRETER, duration, telemetryProperties); } + private getPythonUpdaterService(configTarget: ConfigurationTarget, wkspace?: Uri) { switch (configTarget) { case ConfigurationTarget.Global: { @@ -67,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 d7e6451aeb40..ff42f53bcb5b 100644 --- a/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts +++ b/src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts @@ -1,6 +1,6 @@ import { inject, injectable } from 'inversify'; import { Uri } from 'vscode'; -import { IWorkspaceService } from '../../common/application/types'; +import { IInterpreterPathService } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { GlobalPythonPathUpdaterService } from './services/globalUpdaterService'; import { WorkspaceFolderPythonPathUpdaterService } from './services/workspaceFolderUpdaterService'; @@ -9,17 +9,17 @@ import { IPythonPathUpdaterService, IPythonPathUpdaterServiceFactory } from './t @injectable() export class PythonPathUpdaterServiceFactory implements IPythonPathUpdaterServiceFactory { - private readonly workspaceService: IWorkspaceService; + private readonly interpreterPathService: IInterpreterPathService; constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService); + this.interpreterPathService = serviceContainer.get<IInterpreterPathService>(IInterpreterPathService); } public getGlobalPythonPathConfigurationService(): IPythonPathUpdaterService { - return new GlobalPythonPathUpdaterService(this.workspaceService); + return new GlobalPythonPathUpdaterService(this.interpreterPathService); } public getWorkspacePythonPathConfigurationService(wkspace: Uri): IPythonPathUpdaterService { - return new WorkspacePythonPathUpdaterService(wkspace, this.workspaceService); + return new WorkspacePythonPathUpdaterService(wkspace, this.interpreterPathService); } public getWorkspaceFolderPythonPathConfigurationService(workspaceFolder: Uri): IPythonPathUpdaterService { - return new WorkspaceFolderPythonPathUpdaterService(workspaceFolder, this.workspaceService); + 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..39f4edfde1d6 --- /dev/null +++ b/src/client/interpreter/configuration/recommededEnvironmentService.ts @@ -0,0 +1,205 @@ +// 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']; + private hasRegisteredCommand = false; + constructor(@inject(IExtensionContext) private readonly extensionContext: IExtensionContext) {} + supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean } = { + untrustedWorkspace: true, + virtualWorkspace: false, + }; + + async activate(_resource: Resource, _startupStopWatch?: StopWatch): Promise<void> { + if (this.hasRegisteredCommand) { + return; + } + this.hasRegisteredCommand = true; + 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<string>(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<string, string> = 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<string | undefined>(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<string>(MEMENTO_KEY); + if (!existingData) { + return JSON.stringify(environmentPath ? { [workspaceUri]: environmentPath } : {}); + } + try { + const existingJson: Record<string, string> = 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 8fffa5c77c8c..1cf2a7cc478f 100644 --- a/src/client/interpreter/configuration/services/globalUpdaterService.ts +++ b/src/client/interpreter/configuration/services/globalUpdaterService.ts @@ -1,15 +1,15 @@ -import { IWorkspaceService } from '../../../common/application/types'; +import { ConfigurationTarget } from 'vscode'; +import { IInterpreterPathService } from '../../../common/types'; import { IPythonPathUpdaterService } from '../types'; export class GlobalPythonPathUpdaterService implements IPythonPathUpdaterService { - constructor(private readonly workspaceService: IWorkspaceService) { } - public async updatePythonPath(pythonPath: string): Promise<void> { - const pythonConfig = this.workspaceService.getConfiguration('python'); - const pythonPathValue = pythonConfig.inspect<string>('pythonPath'); + constructor(private readonly interpreterPathService: IInterpreterPathService) {} + public async updatePythonPath(pythonPath: string | undefined): Promise<void> { + const pythonPathValue = this.interpreterPathService.inspect(undefined); if (pythonPathValue && pythonPathValue.globalValue === pythonPath) { return; } - 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 03c531f8dce9..8c9656b3febf 100644 --- a/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts +++ b/src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts @@ -1,21 +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 workspaceService: IWorkspaceService) { - } - public async updatePythonPath(pythonPath: string): Promise<void> { - const pythonConfig = this.workspaceService.getConfiguration('python', this.workspaceFolder); - const pythonPathValue = pythonConfig.inspect<string>('pythonPath'); + constructor(private workspaceFolder: Uri, private readonly interpreterPathService: IInterpreterPathService) {} + public async updatePythonPath(pythonPath: string | undefined): Promise<void> { + const pythonPathValue = this.interpreterPathService.inspect(this.workspaceFolder); if (pythonPathValue && pythonPathValue.workspaceFolderValue === pythonPath) { return; } - if (pythonPath.startsWith(this.workspaceFolder.fsPath)) { - pythonPath = path.relative(this.workspaceFolder.fsPath, pythonPath); - } - 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 81624581d2f2..65bcd0b30e39 100644 --- a/src/client/interpreter/configuration/services/workspaceUpdaterService.ts +++ b/src/client/interpreter/configuration/services/workspaceUpdaterService.ts @@ -1,21 +1,15 @@ -import * as path from 'path'; -import { Uri } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IInterpreterPathService } from '../../../common/types'; import { IPythonPathUpdaterService } from '../types'; export class WorkspacePythonPathUpdaterService implements IPythonPathUpdaterService { - constructor(private workspace: Uri, private readonly workspaceService: IWorkspaceService) { - } - public async updatePythonPath(pythonPath: string): Promise<void> { - const pythonConfig = this.workspaceService.getConfiguration('python', this.workspace); - const pythonPathValue = pythonConfig.inspect<string>('pythonPath'); + constructor(private workspace: Uri, private readonly interpreterPathService: IInterpreterPathService) {} + public async updatePythonPath(pythonPath: string | undefined): Promise<void> { + const pythonPathValue = this.interpreterPathService.inspect(this.workspace); if (pythonPathValue && pythonPathValue.workspaceValue === pythonPath) { return; } - if (pythonPath.startsWith(this.workspace.fsPath)) { - pythonPath = path.relative(this.workspace.fsPath, pythonPath); - } - 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 7b4f6dd26060..05ff8e32c18e 100644 --- a/src/client/interpreter/configuration/types.ts +++ b/src/client/interpreter/configuration/types.ts @@ -1,8 +1,10 @@ -import { ConfigurationTarget, Disposable, Uri } from 'vscode'; -import { PythonInterpreter } from '../contracts'; +import { ConfigurationTarget, Disposable, QuickPickItem, Uri } from 'vscode'; +import { Resource } from '../../common/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { PythonExtension, ResolvedEnvironment } from '../../api/types'; export interface IPythonPathUpdaterService { - updatePythonPath(pythonPath: string): Promise<void>; + updatePythonPath(pythonPath: string | undefined): Promise<void>; } export const IPythonPathUpdaterServiceFactory = Symbol('IPythonPathUpdaterServiceFactory'); @@ -14,15 +16,99 @@ export interface IPythonPathUpdaterServiceFactory { export const IPythonPathUpdaterServiceManager = Symbol('IPythonPathUpdaterServiceManager'); export interface IPythonPathUpdaterServiceManager { - updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', wkspace?: Uri): Promise<void>; + updatePythonPath( + pythonPath: string | undefined, + configTarget: ConfigurationTarget, + trigger: 'ui' | 'shebang' | 'load', + wkspace?: Uri, + ): Promise<void>; } export const IInterpreterSelector = Symbol('IInterpreterSelector'); export interface IInterpreterSelector extends Disposable { - initialize(): void; + getRecommendedSuggestion( + suggestions: IInterpreterQuickPickItem[], + resource: Resource, + ): IInterpreterQuickPickItem | undefined; + /** + * @deprecated Only exists for old Jupyter integration. + */ + getAllSuggestions(resource: Resource): Promise<IInterpreterQuickPickItem[]>; + getSuggestions(resource: Resource, useFullDisplayName?: boolean): IInterpreterQuickPickItem[]; + suggestionToQuickPickItem( + suggestion: PythonEnvironment, + workspaceUri?: Uri | undefined, + useDetailedName?: boolean, + ): IInterpreterQuickPickItem; +} + +export interface IInterpreterQuickPickItem extends QuickPickItem { + path: string; + /** + * The interpreter related to this quickpick item. + * + * @type {PythonEnvironment} + * @memberof IInterpreterQuickPickItem + */ + 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<void>; + 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<string | undefined>; +} + +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 846375450f12..30a05c140249 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -1,144 +1,131 @@ import { SemVer } from 'semver'; -import { CodeLensProvider, ConfigurationTarget, Disposable, Event, TextDocument, Uri } from 'vscode'; -import { InterpreterInfomation } from '../common/process/types'; +import { ConfigurationTarget, Disposable, Event, Uri } from 'vscode'; +import { FileChangeType } from '../common/platform/fileSystemWatcher'; import { Resource } from '../common/types'; +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 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<string>; - getPipVersion(pythonPath: string): Promise<string>; -} - -export const IKnownSearchPathsForInterpreters = Symbol('IKnownSearchPathsForInterpreters'); -export interface IKnownSearchPathsForInterpreters { - getSearchPaths(): string[]; -} -export const IVirtualEnvironmentsSearchPathProvider = Symbol('IVirtualEnvironmentsSearchPathProvider'); -export interface IVirtualEnvironmentsSearchPathProvider { - getSearchPaths(resource?: Uri): Promise<string[]>; -} -export const IInterpreterLocatorService = Symbol('IInterpreterLocatorService'); +export const IComponentAdapter = Symbol('IComponentAdapter'); +export interface IComponentAdapter { + readonly onProgress: Event<ProgressNotificationEvent>; + triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise<void>; + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined; + readonly onChanged: Event<PythonEnvironmentsChangedEvent>; + // VirtualEnvPrompt + onDidCreate(resource: Resource, callback: () => void): Disposable; + // IInterpreterLocatorService + hasInterpreters(filter?: (e: PythonEnvironment) => Promise<boolean>): Promise<boolean>; + getInterpreters(resource?: Uri, source?: PythonEnvSource[]): PythonEnvironment[]; + + // WorkspaceVirtualEnvInterpretersAutoSelectionRule + getWorkspaceVirtualEnvInterpreters( + resource: Uri, + options?: { ignoreCache?: boolean }, + ): Promise<PythonEnvironment[]>; + + // IInterpreterService + getInterpreterDetails(pythonPath: string): Promise<PythonEnvironment | undefined>; + + // IInterpreterHelper + // Undefined is expected on this API, if the environment info retrieval fails. + getInterpreterInformation(pythonPath: string): Promise<Partial<PythonEnvironment> | undefined>; + + isMacDefaultPythonPath(pythonPath: string): Promise<boolean>; + + // ICondaService + isCondaEnvironment(interpreterPath: string): Promise<boolean>; + // Undefined is expected on this API, if the environment is not conda env. + getCondaEnvironment(interpreterPath: string): Promise<CondaEnvironmentInfo | undefined>; -export interface IInterpreterLocatorService extends Disposable { - readonly onLocating: Event<Promise<PythonInterpreter[]>>; - readonly hasInterpreters: Promise<boolean>; - getInterpreters(resource?: Uri, ignoreCache?: boolean): Promise<PythonInterpreter[]>; + isMicrosoftStoreInterpreter(pythonPath: string): Promise<boolean>; } -export type CondaInfo = { - envs?: string[]; - 'sys.version'?: string; - 'sys.prefix'?: string; - 'python_version'?: string; - default_prefix?: string; - conda_version?: string; -}; - 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<string>; + getCondaFile(forShellExecution?: boolean): Promise<string>; + getCondaInfo(): Promise<CondaInfo | undefined>; isCondaAvailable(): Promise<boolean>; getCondaVersion(): Promise<SemVer | undefined>; - getCondaInfo(): Promise<CondaInfo | undefined>; - getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string; path: string }[]) | undefined>; - getInterpreterPath(condaEnvironmentPath: string): string; - isCondaEnvironment(interpreterPath: string): Promise<boolean>; - getCondaEnvironment(interpreterPath: string): Promise<{ name: string; path: string } | undefined>; - getActivatedCondaEnvironment(interpreter: PythonInterpreter, inputEnvironment?: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv>; -} - -export enum InterpreterType { - Unknown = 'Unknown', - Conda = 'Conda', - VirtualEnv = 'VirtualEnv', - PipEnv = 'PipEnv', - Pyenv = 'Pyenv', - Venv = 'Venv' + getInterpreterPathForEnvironment(condaEnv: CondaEnvironmentInfo): Promise<string | undefined>; + getCondaFileFromInterpreter(interpreterPath?: string, envName?: string): Promise<string | undefined>; + getActivationScriptFromInterpreter( + interpreterPath?: string, + envName?: string, + ): Promise<{ path: string | undefined; type: 'local' | 'global' } | undefined>; } -export type PythonInterpreter = InterpreterInfomation & { - companyDisplayName?: string; - displayName?: string; - type: InterpreterType; - envName?: string; - envPath?: string; - cachedEntry?: boolean; -}; - -export type WorkspacePythonPath = { - folderUri: Uri; - configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder; -}; export const IInterpreterService = Symbol('IInterpreterService'); export interface IInterpreterService { - onDidChangeInterpreter: Event<void>; - hasInterpreters: Promise<boolean>; - getInterpreters(resource?: Uri): Promise<PythonInterpreter[]>; - getActiveInterpreter(resource?: Uri): Promise<PythonInterpreter | undefined>; - getInterpreterDetails(pythonPath: string, resoure?: Uri): Promise<undefined | PythonInterpreter>; - refresh(resource: Uri | undefined): Promise<void>; + triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise<void>; + readonly refreshPromise: Promise<void> | undefined; + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined; + readonly onDidChangeInterpreters: Event<PythonEnvironmentsChangedEvent>; + onDidChangeInterpreterConfiguration: Event<Uri | undefined>; + onDidChangeInterpreter: Event<Uri | undefined>; + onDidChangeInterpreterInformation: Event<PythonEnvironment>; + /** + * 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<boolean>): Promise<boolean>; + getInterpreters(resource?: Uri): PythonEnvironment[]; + /** + * @deprecated Only exists for old Jupyter integration. + */ + getAllInterpreters(resource?: Uri): Promise<PythonEnvironment[]>; + getActiveInterpreter(resource?: Uri): Promise<PythonEnvironment | undefined>; + getInterpreterDetails(pythonPath: string, resoure?: Uri): Promise<undefined | PythonEnvironment>; + refresh(resource: Resource): Promise<void>; initialize(): void; - getDisplayName(interpreter: Partial<PythonInterpreter>): Promise<string>; } export const IInterpreterDisplay = Symbol('IInterpreterDisplay'); export interface IInterpreterDisplay { refresh(resource?: Uri): Promise<void>; -} - -export const IShebangCodeLensProvider = Symbol('IShebangCodeLensProvider'); -export interface IShebangCodeLensProvider extends CodeLensProvider { - detectShebang(document: TextDocument): Promise<string | undefined>; + registerVisibilityFilter(filter: IInterpreterStatusbarVisibilityFilter): void; } export const IInterpreterHelper = Symbol('IInterpreterHelper'); export interface IInterpreterHelper { getActiveWorkspaceUri(resource: Resource): WorkspacePythonPath | undefined; - getInterpreterInformation(pythonPath: string): Promise<undefined | Partial<PythonInterpreter>>; - isMacDefaultPythonPath(pythonPath: string): Boolean; - getInterpreterTypeDisplayName(interpreterType: InterpreterType): string | undefined; - getBestInterpreter(interpreters?: PythonInterpreter[]): PythonInterpreter | undefined; -} - -export const IPipEnvService = Symbol('IPipEnvService'); -export interface IPipEnvService { - isRelatedPipEnvironment(dir: string, pythonPath: string): Promise<boolean>; -} - -export const IInterpreterLocatorHelper = Symbol('IInterpreterLocatorHelper'); -export interface IInterpreterLocatorHelper { - mergeInterpreters(interpreters: PythonInterpreter[]): PythonInterpreter[]; -} - -export const IInterpreterWatcher = Symbol('IInterpreterWatcher'); -export interface IInterpreterWatcher { - onDidCreate: Event<void>; + getInterpreterInformation(pythonPath: string): Promise<undefined | Partial<PythonEnvironment>>; + isMacDefaultPythonPath(pythonPath: string): Promise<boolean>; + getInterpreterTypeDisplayName(interpreterType: EnvironmentType): string | undefined; + getBestInterpreter(interpreters?: PythonEnvironment[]): PythonEnvironment | undefined; } -export const IInterpreterWatcherBuilder = Symbol('IInterpreterWatcherBuilder'); -export interface IInterpreterWatcherBuilder { - getWorkspaceVirtualEnvInterpreterWatcher(resource: Uri | undefined): Promise<IInterpreterWatcher>; +export const IInterpreterStatusbarVisibilityFilter = Symbol('IInterpreterStatusbarVisibilityFilter'); +/** + * Implement this interface to control the visibility of the interpreter statusbar. + */ +export interface IInterpreterStatusbarVisibilityFilter { + readonly changed?: Event<void>; + readonly hidden: boolean; } -export const InterpreterLocatorProgressHandler = Symbol('InterpreterLocatorProgressHandler'); -export interface InterpreterLocatorProgressHandler { - register(): void; -} +export type WorkspacePythonPath = { + folderUri: Uri; + configTarget: ConfigurationTarget.Workspace | ConfigurationTarget.WorkspaceFolder; +}; -export const IInterpreterLocatorProgressService = Symbol('IInterpreterLocatorProgressService'); -export interface IInterpreterLocatorProgressService { - readonly onRefreshing: Event<void>; - readonly onRefreshed: Event<void>; - register(): void; +export const IActivatedEnvironmentLaunch = Symbol('IActivatedEnvironmentLaunch'); +export interface IActivatedEnvironmentLaunch { + selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection?: boolean): Promise<string | undefined>; } diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index 975c4a60473f..3a602093d4f9 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -1,32 +1,96 @@ import { inject, injectable } from 'inversify'; -import { Disposable, StatusBarAlignment, StatusBarItem, Uri } from 'vscode'; +import { + Disposable, + l10n, + LanguageStatusItem, + LanguageStatusSeverity, + StatusBarAlignment, + StatusBarItem, + ThemeColor, + Uri, +} from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; -import { IDisposableRegistry, IPathUtils } from '../../common/types'; +import { Commands, PYTHON_LANGUAGE } from '../../common/constants'; +import '../../common/extensions'; +import { IDisposableRegistry, IPathUtils, Resource } from '../../common/types'; +import { InterpreterQuickPickList, Interpreters } from '../../common/utils/localize'; import { IServiceContainer } from '../../ioc/types'; -import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService } from '../contracts'; +import { traceLog } from '../../logging'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { + IInterpreterDisplay, + IInterpreterHelper, + IInterpreterService, + IInterpreterStatusbarVisibilityFilter, +} from '../contracts'; +import { shouldEnvExtHandleActivation } from '../../envExt/api.internal'; + +/** + * 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; -// 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 statusBarCanBeDisplayed?: boolean; + private visibilityFilters: IInterpreterStatusbarVisibilityFilter[] = []; + private disposableRegistry: Disposable[]; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { this.helper = serviceContainer.get<IInterpreterHelper>(IInterpreterHelper); this.workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService); this.pathUtils = serviceContainer.get<IPathUtils>(IPathUtils); this.interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService); - const application = serviceContainer.get<IApplicationShell>(IApplicationShell); - const disposableRegistry = serviceContainer.get<Disposable[]>(IDisposableRegistry); + this.disposableRegistry = serviceContainer.get<Disposable[]>(IDisposableRegistry); + + this.interpreterService.onDidChangeInterpreterInformation( + this.onDidChangeInterpreterInformation, + this, + this.disposableRegistry, + ); + } - this.statusBar = application.createStatusBarItem(StatusBarAlignment.Left, 100); - this.statusBar.command = 'python.setInterpreter'; - disposableRegistry.push(this.statusBar); + public async activate(): Promise<void> { + if (shouldEnvExtHandleActivation()) { + return; + } + const application = this.serviceContainer.get<IApplicationShell>(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)) { @@ -38,17 +102,94 @@ export class InterpreterDisplay implements IInterpreterDisplay { } await this.updateDisplay(resource); } + public registerVisibilityFilter(filter: IInterpreterStatusbarVisibilityFilter) { + const disposableRegistry = this.serviceContainer.get<Disposable[]>(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) { + if (shouldEnvExtHandleActivation()) { + this.statusBar?.hide(); + this.languageStatus?.dispose(); + this.languageStatus = undefined; + return; + } const interpreter = await this.interpreterService.getActiveInterpreter(workspaceFolder); - if (interpreter) { - this.statusBar.color = ''; - this.statusBar.tooltip = this.pathUtils.getDisplayName(interpreter.path, workspaceFolder ? workspaceFolder.fsPath : undefined); - this.statusBar.text = interpreter.displayName!; + if ( + this.currentlySelectedInterpreterDisplay && + this.currentlySelectedInterpreterDisplay === interpreter?.detailedDisplayName && + this.currentlySelectedInterpreterPath === interpreter.path + ) { + return; + } + this.currentlySelectedWorkspaceFolder = workspaceFolder; + 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.statusBarCanBeDisplayed = true; + this.updateVisibility(); + } + private updateVisibility() { + if (!this.statusBar || !this.statusBarCanBeDisplayed) { + return; + } + if (this.visibilityFilters.length === 0 || this.visibilityFilters.every((filter) => !filter.hidden)) { + this.statusBar.show(); } else { - this.statusBar.tooltip = ''; - this.statusBar.color = 'yellow'; - this.statusBar.text = '$(alert) Select Python Interpreter'; + this.statusBar.hide(); } - this.statusBar.show(); } } diff --git a/src/client/interpreter/display/progressDisplay.ts b/src/client/interpreter/display/progressDisplay.ts index 325568a01b72..4b2811043d2f 100644 --- a/src/client/interpreter/display/progressDisplay.ts +++ b/src/client/interpreter/display/progressDisplay.ts @@ -5,41 +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 { IInterpreterLocatorProgressService, InterpreterLocatorProgressHandler } 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 InterpreterLocatorProgressHandler { +export class InterpreterLocatorProgressStatusBarHandler implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + private deferred: Deferred<void> | undefined; + private isFirstTimeLoadingInterpreters = true; - constructor(@inject(IApplicationShell) private readonly shell: IApplicationShell, - @inject(IInterpreterLocatorProgressService) private readonly progressService: IInterpreterLocatorProgressService, - @inject(IDisposableRegistry) private readonly disposables: Disposable[]) { } - public register() { - this.progressService.onRefreshing(() => this.showProgress(), this, this.disposables); - this.progressService.onRefreshed(() => this.hideProgress(), this, this.disposables); + + constructor( + @inject(IApplicationShell) private readonly shell: IApplicationShell, + @inject(IDisposableRegistry) private readonly disposables: Disposable[], + @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, + ) {} + + public async activate(): Promise<void> { + 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 e3821f4afea0..000000000000 --- a/src/client/interpreter/display/shebangCodeLensProvider.ts +++ /dev/null @@ -1,69 +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 { IProcessServiceFactory } from '../../common/process/types'; -import { IConfigurationService } from '../../common/types'; -import { IShebangCodeLensProvider } from '../contracts'; - -@injectable() -export class ShebangCodeLensProvider implements IShebangCodeLensProvider { - public readonly onDidChangeCodeLenses: Event<void>; - 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<void>; - - } - public async detectShebang(document: TextDocument): Promise<string | undefined> { - const firstLine = document.lineAt(0); - if (firstLine.isEmptyOrWhitespace) { - return; - } - - if (!firstLine.text.startsWith('#!')) { - return; - } - - const shebang = firstLine.text.substr(2).trim(); - const pythonPath = await this.getFullyQualifiedPathToInterpreter(shebang, document.uri); - return typeof pythonPath === 'string' && pythonPath.length > 0 ? pythonPath : undefined; - } - public async provideCodeLenses(document: TextDocument, _token?: CancellationToken): Promise<CodeLens[]> { - return this.createShebangCodeLens(document); - } - private async getFullyQualifiedPathToInterpreter(pythonPath: string, resource: Uri) { - let cmdFile = pythonPath; - let args = ['-c', 'import sys;print(sys.executable)']; - 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 = parts.concat(args); - } - const processService = await this.processServiceFactory.create(resource); - return processService.exec(cmdFile, args) - .then(output => output.stdout.trim()) - .catch(() => ''); - } - private async createShebangCodeLens(document: TextDocument) { - const shebang = await this.detectShebang(document); - const pythonPath = this.configurationService.getSettings(document.uri).pythonPath; - if (!shebang || shebang === pythonPath) { - 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 9607cc499901..413fa225f3ef 100644 --- a/src/client/interpreter/helpers.ts +++ b/src/client/interpreter/helpers.ts @@ -1,35 +1,47 @@ import { inject, injectable } from 'inversify'; -import { compare } from 'semver'; -import { ConfigurationTarget } from 'vscode'; +import { ConfigurationTarget, Uri } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { IFileSystem } from '../common/platform/types'; -import { InterpreterInfomation, IPythonExecutionFactory } from '../common/process/types'; -import { IPersistentStateFactory, Resource } from '../common/types'; +import { FileSystemPaths } from '../common/platform/fs-paths'; +import { Resource } from '../common/types'; import { IServiceContainer } from '../ioc/types'; -import { IInterpreterHelper, InterpreterType, PythonInterpreter, WorkspacePythonPath } from './contracts'; +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<PythonInterpreter> & { fileHash: string }; +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); +} -export function getFirstNonEmptyLineFromMultilineString(stdout: string) { - if (!stdout) { - return ''; +/** + * 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 lines = stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); - return lines.length > 0 ? lines[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 fs: IFileSystem; - private readonly persistentFactory: IPersistentStateFactory; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.persistentFactory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory); - this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem); - } + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, + ) {} + public getActiveWorkspaceUri(resource: Resource): WorkspacePythonPath | undefined { const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - if (!workspaceService.hasWorkspaceFolders) { + const hasWorkspaceFolders = (workspaceService.workspaceFolders?.length || 0) > 0; + if (!hasWorkspaceFolders) { return; } if (Array.isArray(workspaceService.workspaceFolders) && workspaceService.workspaceFolders.length === 1) { @@ -51,65 +63,40 @@ export class InterpreterHelper implements IInterpreterHelper { } } } - public async getInterpreterInformation(pythonPath: string): Promise<undefined | Partial<PythonInterpreter>> { - let fileHash = await this.fs.getFileHash(pythonPath).catch(() => ''); - fileHash = fileHash ? fileHash : ''; - const store = this.persistentFactory.createGlobalPersistentState<CachedPythonInterpreter>(`${pythonPath}.v3`, undefined, EXPITY_DURATION); - if (store.value && fileHash && store.value.fileHash === fileHash) { - return store.value; - } - const processService = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ pythonPath }); - try { - const info = await processService.getInterpreterInformation().catch<InterpreterInfomation | undefined>(() => undefined); - if (!info) { - return; - } - const details = { - ...(info), - fileHash - }; - await store.updateValue(details); - return details; - } catch (ex) { - console.error(`Failed to get interpreter information for '${pythonPath}'`, ex); - return; - } + public async getInterpreterInformation(pythonPath: string): Promise<undefined | Partial<PythonEnvironment>> { + return this.pyenvs.getInterpreterInformation(pythonPath); } - public isMacDefaultPythonPath(pythonPath: string) { - return pythonPath === 'python' || pythonPath === '/usr/bin/python'; + + public async getInterpreters({ resource, source }: { resource?: Uri; source?: PythonEnvSource[] } = {}): Promise< + PythonEnvironment[] + > { + const interpreters = await this.pyenvs.getInterpreters(resource, source); + return sortInterpreters(interpreters); } - public getInterpreterTypeDisplayName(interpreterType: InterpreterType) { - switch (interpreterType) { - case InterpreterType.Conda: { - return 'conda'; - } - case InterpreterType.PipEnv: { - return 'pipenv'; - } - case InterpreterType.Pyenv: { - return 'pyenv'; - } - case InterpreterType.Venv: { - return 'venv'; - } - case InterpreterType.VirtualEnv: { - return 'virtualenv'; - } - default: { - return ''; - } + + public async getInterpreterPath(pythonPath: string): Promise<string> { + const interpreterInfo: any = await this.getInterpreterInformation(pythonPath); + if (interpreterInfo) { + return interpreterInfo.path; + } else { + return pythonPath; } } - public getBestInterpreter(interpreters?: PythonInterpreter[]): PythonInterpreter | undefined { + + public async isMacDefaultPythonPath(pythonPath: string): Promise<boolean> { + return this.pyenvs.isMacDefaultPythonPath(pythonPath); + } + + public getInterpreterTypeDisplayName(interpreterType: EnvironmentType): string { + return getEnvironmentTypeName(interpreterType); + } + + public getBestInterpreter(interpreters?: PythonEnvironment[]): PythonEnvironment | undefined { if (!Array.isArray(interpreters) || interpreters.length === 0) { return; } - if (interpreters.length === 1) { - return interpreters[0]; - } - const sorted = interpreters.slice(); - sorted.sort((a, b) => (a.version && b.version) ? compare(a.version.raw, b.version.raw) : 0); + const sorted = sortInterpreters(interpreters); return sorted[sorted.length - 1]; } } 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<void> { + this.disposables.push( + registerCommand(Commands.GetSelectedInterpreterPath, (args) => this._getSelectedInterpreterPath(args)), + ); + } + + public async _getSelectedInterpreterPath( + args: { workspaceFolder: string; type: string } | string[], + ): Promise<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 + 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 95d0fb57654d..ad06fd7d051d 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -1,237 +1,322 @@ +// eslint-disable-next-line max-classes-per-file import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Disposable, Event, EventEmitter, Uri } from 'vscode'; -import '../../client/common/extensions'; -import { IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { getArchitectureDisplayName } from '../common/platform/registry'; -import { IFileSystem } from '../common/platform/types'; -import { IPythonExecutionFactory } from '../common/process/types'; -import { IConfigurationService, IDisposableRegistry, IPersistentStateFactory } from '../common/types'; -import { sleep } from '../common/utils/async'; +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, + IInstaller, + IInterpreterPathService, + Product, +} from '../common/types'; import { IServiceContainer } from '../ioc/types'; -import { captureTelemetry } from '../telemetry'; -import { PYTHON_INTERPRETER_DISCOVERY } from '../telemetry/constants'; +import { PythonEnvironment } from '../pythonEnvironments/info'; import { - IInterpreterDisplay, IInterpreterHelper, IInterpreterLocatorService, - IInterpreterService, INTERPRETER_LOCATOR_SERVICE, - InterpreterType, PythonInterpreter} from './contracts'; -import { IVirtualEnvironmentManager } from './virtualEnvs/types'; + IActivatedEnvironmentLaunch, + IComponentAdapter, + IInterpreterDisplay, + IInterpreterService, + IInterpreterStatusbarVisibilityFilter, + PythonEnvironmentsChangedEvent, +} from './contracts'; +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'; -const EXPITY_DURATION = 24 * 60 * 60 * 1000; +type StoredPythonEnvironment = PythonEnvironment & { store?: boolean }; @injectable() export class InterpreterService implements Disposable, IInterpreterService { - private readonly locator: IInterpreterLocatorService; - private readonly fs: IFileSystem; - private readonly persistentStateFactory: IPersistentStateFactory; - private readonly configService: IConfigurationService; - private readonly didChangeInterpreterEmitter = new EventEmitter<void>(); - private pythonPathSetting: string = ''; + public async hasInterpreters( + filter: (e: PythonEnvironment) => Promise<boolean> = async () => true, + ): Promise<boolean> { + return this.pyenvs.hasInterpreters(filter); + } - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.locator = serviceContainer.get<IInterpreterLocatorService>(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); - this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem); - this.persistentStateFactory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory); - this.configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); + public triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise<void> { + return this.pyenvs.triggerRefresh(query, options); } - public get hasInterpreters(): Promise<boolean> { - return this.locator.hasInterpreters; + + public get refreshPromise(): Promise<void> | undefined { + return this.pyenvs.getRefreshPromise(); } - public async refresh(resource?: Uri) { - const interpreterDisplay = this.serviceContainer.get<IInterpreterDisplay>(IInterpreterDisplay); - return interpreterDisplay.refresh(resource); + public getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined { + return this.pyenvs.getRefreshPromise(options); } - public initialize() { - const disposables = this.serviceContainer.get<Disposable[]>(IDisposableRegistry); - const documentManager = this.serviceContainer.get<IDocumentManager>(IDocumentManager); - disposables.push(documentManager.onDidChangeActiveTextEditor((e) => e ? this.refresh(e.document.uri) : undefined)); - const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - const pySettings = this.configService.getSettings(); - this.pythonPathSetting = pySettings.pythonPath; - const disposable = workspaceService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('python.pythonPath', undefined)) { - this.onConfigChanged(); - } - }); - disposables.push(disposable); + public get onDidChangeInterpreter(): Event<Uri | undefined> { + return this.didChangeInterpreterEmitter.event; } - @captureTelemetry(PYTHON_INTERPRETER_DISCOVERY, { locator: 'all' }, true) - public async getInterpreters(resource?: Uri): Promise<PythonInterpreter[]> { - const interpreters = await this.locator.getInterpreters(resource); - await Promise.all(interpreters - .filter(item => !item.displayName) - .map(async item => item.displayName = await this.getDisplayName(item, resource))); - return interpreters; + public onDidChangeInterpreters: Event<PythonEnvironmentsChangedEvent>; + + public get onDidChangeInterpreterInformation(): Event<PythonEnvironment> { + return this.didChangeInterpreterInformation.event; } - public dispose(): void { - this.locator.dispose(); - this.didChangeInterpreterEmitter.dispose(); + public get onDidChangeInterpreterConfiguration(): Event<Uri | undefined> { + return this.didChangeInterpreterConfigurationEmitter.event; } - public get onDidChangeInterpreter(): Event<void> { - return this.didChangeInterpreterEmitter.event; + public _pythonPathSetting: string | undefined = ''; + + private readonly didChangeInterpreterConfigurationEmitter = new EventEmitter<Uri | undefined>(); + + private readonly configService: IConfigurationService; + + private readonly interpreterPathService: IInterpreterPathService; + + private readonly didChangeInterpreterEmitter = new EventEmitter<Uri | undefined>(); + + private readonly didChangeInterpreterInformation = new EventEmitter<PythonEnvironment>(); + + private readonly activeInterpreterPaths = new Map< + string, + { path: string; workspaceFolder: WorkspaceFolder | undefined } + >(); + + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, + ) { + this.configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); + this.interpreterPathService = this.serviceContainer.get<IInterpreterPathService>(IInterpreterPathService); + this.onDidChangeInterpreters = pyenvs.onChanged; } - public async getActiveInterpreter(resource?: Uri): Promise<PythonInterpreter | undefined> { - const pythonExecutionFactory = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); - const pythonExecutionService = await pythonExecutionFactory.create({ resource }); - const fullyQualifiedPath = await pythonExecutionService.getExecutablePath().catch(() => undefined); - // Python path is invalid or python isn't installed. - if (!fullyQualifiedPath) { - return; - } + public async refresh(resource?: Uri): Promise<void> { + const interpreterDisplay = this.serviceContainer.get<IInterpreterDisplay>(IInterpreterDisplay); + await interpreterDisplay.refresh(resource); + const workspaceFolder = this.serviceContainer + .get<IWorkspaceService>(IWorkspaceService) + .getWorkspaceFolder(resource); + const path = this.configService.getSettings(resource).pythonPath; + const workspaceKey = this.serviceContainer + .get<IWorkspaceService>(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path, workspaceFolder }); + this.ensureEnvironmentContainsPython(path, workspaceFolder).ignoreErrors(); + } - return this.getInterpreterDetails(fullyQualifiedPath, resource); - } - public async getInterpreterDetails(pythonPath: string, resource?: Uri): Promise<PythonInterpreter | undefined> { - // If we don't have the fully qualified path, then get it. - if (path.basename(pythonPath) === pythonPath) { - const pythonExecutionFactory = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); - const pythonExecutionService = await pythonExecutionFactory.create({ resource }); - pythonPath = await pythonExecutionService.getExecutablePath().catch(() => ''); - // Python path is invalid or python isn't installed. - if (!pythonPath) { - return; + public initialize(): void { + const disposables = this.serviceContainer.get<Disposable[]>(IDisposableRegistry); + const documentManager = this.serviceContainer.get<IDocumentManager>(IDocumentManager); + const interpreterDisplay = this.serviceContainer.get<IInterpreterDisplay>(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 fileHash = await this.fs.getFileHash(pythonPath).catch(() => '') || ''; - const store = this.persistentStateFactory.createGlobalPersistentState<PythonInterpreter & { fileHash: string }>(`${pythonPath}.interpreter.details.v5`, undefined, EXPITY_DURATION); - if (store.value && fileHash && store.value.fileHash === fileHash) { - return store.value; - } + public readonly interpreterVisibilityEmitter = new EventEmitter<void>(); + + public readonly changed = this.interpreterVisibilityEmitter.event; - const fs = this.serviceContainer.get<IFileSystem>(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; + 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; } - // Use option1 as a fallback. - // tslint:disable-next-line:no-any - return option1 as any as PythonInterpreter; - })(); - - const interpreterInfo = await Promise.race([option2, option1]) as PythonInterpreter; - - // tslint:disable-next-line:no-any - if (interpreterInfo && (interpreterInfo as any).__store) { - await store.updateValue({ ...interpreterInfo, path: pythonPath, fileHash }); - } else { - // If we got information from option1, then when option2 finishes cache it for later use (ignoring erors); - option2.then(info => { - // tslint:disable-next-line:no-any - if (info && (info as any).__store) { - return store.updateValue({ ...info, path: pythonPath, fileHash }); + })(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, + }); + } + } } - }).ignoreErrors(); - } - return interpreterInfo; - } - /** - * Gets the display name of an interpreter. - * The format is `Python <Version> <bitness> (<env name>: <env type>)` - * E.g. `Python 3.5.1 32-bit (myenv2: virtualenv)` - * @param {Partial<PythonInterpreter>} info - * @returns {string} - * @memberof InterpreterService - */ - public async getDisplayName(info: Partial<PythonInterpreter>, resource?: Uri): Promise<string> { - const fileHash = (info.path ? await this.fs.getFileHash(info.path).catch(() => '') : '') || ''; - const store = this.persistentStateFactory.createGlobalPersistentState<{ fileHash: string; displayName: string }>(`${info.path}${fileHash}.interpreter.displayName.v5`, undefined, EXPITY_DURATION); - if (store.value && store.value.fileHash === fileHash && store.value.displayName) { - return store.value.displayName; - } - const displayNameParts: string[] = ['Python']; - const envSuffixParts: string[] = []; + }), + ); + 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); + } + }), + ); + disposables.push(this.interpreterPathService.onDidChange((i) => this._onConfigChanged(i.uri))); + } - 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>(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>(IInterpreterHelper); - const name = interpreterHelper.getInterpreterTypeDisplayName(info.type); - if (name) { - envSuffixParts.push(name); - } - } + public getInterpreters(resource?: Uri): PythonEnvironment[] { + return this.pyenvs.getInterpreters(resource); + } + + public async getAllInterpreters(resource?: Uri): Promise<PythonEnvironment[]> { + // 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); + } + + public dispose(): void { + this.didChangeInterpreterEmitter.dispose(); + this.didChangeInterpreterInformation.dispose(); + } - const envSuffix = envSuffixParts.length === 0 ? '' : - `(${envSuffixParts.join(': ')})`; - const displayName = `${displayNameParts.join(' ')} ${envSuffix}`.trim(); + public async getActiveInterpreter(resource?: Uri): Promise<PythonEnvironment | undefined> { + if (useEnvExtension()) { + return getActiveInterpreterLegacy(resource); + } - // If dealing with cached entry, then do not store the display name in cache. - if (!info.cachedEntry) { - await store.updateValue({ displayName, fileHash }); + const activatedEnvLaunch = this.serviceContainer.get<IActivatedEnvironmentLaunch>(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>( + 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; + } } + return this.getInterpreterDetails(path); + } - return displayName; + public async getInterpreterDetails(pythonPath: string): Promise<StoredPythonEnvironment | undefined> { + return this.pyenvs.getInterpreterDetails(pythonPath); } - private onConfigChanged = () => { - // Check if we actually changed our python path - const pySettings = this.configService.getSettings(); - if (this.pythonPathSetting !== pySettings.pythonPath) { - this.pythonPathSetting = pySettings.pythonPath; - this.didChangeInterpreterEmitter.fire(); + + public async _onConfigChanged(resource?: Uri): Promise<void> { + // 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(resource); + const workspaceFolder = this.serviceContainer + .get<IWorkspaceService>(IWorkspaceService) + .getWorkspaceFolder(resource); + reportActiveInterpreterChanged({ + path: pySettings.pythonPath, + resource: workspaceFolder, + }); + const workspaceKey = this.serviceContainer + .get<IWorkspaceService>(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path: pySettings.pythonPath, workspaceFolder }); const interpreterDisplay = this.serviceContainer.get<IInterpreterDisplay>(IInterpreterDisplay); - interpreterDisplay.refresh() - .catch(ex => console.error('Python Extension: display.refresh', ex)); + interpreterDisplay.refresh().catch((ex) => traceError('Python Extension: display.refresh', ex)); + await this.ensureEnvironmentContainsPython(this._pythonPathSetting, workspaceFolder); } } - private async collectInterpreterDetails(pythonPath: string, resource: Uri | undefined) { - const interpreterHelper = this.serviceContainer.get<IInterpreterHelper>(IInterpreterHelper); - const virtualEnvManager = this.serviceContainer.get<IVirtualEnvironmentManager>(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<PythonInterpreter> = { - ...(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>(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>(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 2bfb72c4f98f..000000000000 --- a/src/client/interpreter/interpreterVersion.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { inject, injectable } from 'inversify'; -import '../common/extensions'; -import { IProcessServiceFactory } from '../common/process/types'; -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<string> { - const processService = await this.processServiceFactory.create(); - return processService.exec(pythonPath, ['--version'], { mergeStdOutErr: true }) - .then(output => output.stdout.splitLines()[0]) - .then(version => version.length === 0 ? defaultValue : version) - .catch(() => defaultValue); - } - public async getPipVersion(pythonPath: string): Promise<string> { - const processService = await this.processServiceFactory.create(); - const output = await processService.exec(pythonPath, ['-m', 'pip', '--version'], { mergeStdOutErr: true }); - if (output.stdout.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(output.stdout); - 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/helpers.ts b/src/client/interpreter/locators/helpers.ts deleted file mode 100644 index 401aa02d18f0..000000000000 --- a/src/client/interpreter/locators/helpers.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { IS_WINDOWS } from '../../common/platform/constants'; -import { IFileSystem } from '../../common/platform/types'; -import { fsReaddirAsync } from '../../common/utils/fs'; -import { IServiceContainer } from '../../ioc/types'; -import { IInterpreterLocatorHelper, InterpreterType, PythonInterpreter } from '../contracts'; - -const CheckPythonInterpreterRegEx = IS_WINDOWS ? /^python(\d+(.\d+)?)?\.exe$/ : /^python(\d+(.\d+)?)?$/; - -export function lookForInterpretersInDirectory(pathToCheck: string): Promise<string[]> { - return fsReaddirAsync(pathToCheck) - .then(subDirs => subDirs.filter(fileName => CheckPythonInterpreterRegEx.test(path.basename(fileName)))) - .catch(err => { - console.error('Python Extension (lookForInterpretersInDirectory.fsReaddirAsync):', err); - return [] as string[]; - }); -} - -@injectable() -export class InterpreterLocatorHelper implements IInterpreterLocatorHelper { - private readonly fs: IFileSystem; - - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.fs = serviceContainer.get<IFileSystem>(IFileSystem); - } - public mergeInterpreters(interpreters: PythonInterpreter[]) { - return interpreters - .map(item => { return { ...item }; }) - .map(item => { item.path = path.normalize(item.path); return item; }) - .reduce<PythonInterpreter[]>((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]) { - existingItem[prop] = current[prop]; - } - } - } - return accumulator; - }, []); - } -} diff --git a/src/client/interpreter/locators/index.ts b/src/client/interpreter/locators/index.ts deleted file mode 100644 index 8111210e3d85..000000000000 --- a/src/client/interpreter/locators/index.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { inject, injectable } from 'inversify'; -import { Disposable, Event, EventEmitter, Uri } from 'vscode'; -import { IPlatformService } from '../../common/platform/types'; -import { IDisposableRegistry } from '../../common/types'; -import { createDeferred, Deferred } from '../../common/utils/async'; -import { OSType } from '../../common/utils/platform'; -import { IServiceContainer } from '../../ioc/types'; -import { - CONDA_ENV_FILE_SERVICE, - CONDA_ENV_SERVICE, - CURRENT_PATH_SERVICE, - GLOBAL_VIRTUAL_ENV_SERVICE, - IInterpreterLocatorHelper, - IInterpreterLocatorService, - KNOWN_PATH_SERVICE, - PIPENV_SERVICE, - PythonInterpreter, - WINDOWS_REGISTRY_SERVICE, - WORKSPACE_VIRTUAL_ENV_SERVICE -} from '../contracts'; -// tslint:disable-next-line:no-require-imports no-var-requires -const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); - -/** - * Facilitates locating Python interpreters. - */ -@injectable() -export class PythonInterpreterLocatorService implements IInterpreterLocatorService { - private readonly disposables: Disposable[] = []; - private readonly platform: IPlatformService; - private readonly interpreterLocatorHelper: IInterpreterLocatorHelper; - private readonly _hasInterpreters: Deferred<boolean>; - constructor( - @inject(IServiceContainer) private serviceContainer: IServiceContainer - ) { - this._hasInterpreters = createDeferred<boolean>(); - serviceContainer.get<Disposable[]>(IDisposableRegistry).push(this); - this.platform = serviceContainer.get<IPlatformService>(IPlatformService); - this.interpreterLocatorHelper = serviceContainer.get<IInterpreterLocatorHelper>(IInterpreterLocatorHelper); - } - /** - * 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<Promise<PythonInterpreter[]>>} - * @memberof PythonInterpreterLocatorService - */ - public get onLocating(): Event<Promise<PythonInterpreter[]>> { - return new EventEmitter<Promise<PythonInterpreter[]>>().event; - } - public get hasInterpreters(): Promise<boolean> { - return this._hasInterpreters.promise; - } - - /** - * 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. - */ - public async getInterpreters(resource?: Uri): Promise<PythonInterpreter[]> { - const locators = this.getLocators(); - 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!); - 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(): 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] - ]; - return keys - .filter(item => item[1] === undefined || item[1] === this.platform.osType) - .map(item => this.serviceContainer.get<IInterpreterLocatorService>(IInterpreterLocatorService, item[0])); - } -} diff --git a/src/client/interpreter/locators/progressService.ts b/src/client/interpreter/locators/progressService.ts deleted file mode 100644 index 2e68b2194ccf..000000000000 --- a/src/client/interpreter/locators/progressService.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { Disposable, Event, EventEmitter } from 'vscode'; -import { traceDecorators } from '../../common/logger'; -import { IDisposableRegistry } from '../../common/types'; -import { createDeferredFrom, Deferred } from '../../common/utils/async'; -import { noop } from '../../common/utils/misc'; -import { IServiceContainer } from '../../ioc/types'; -import { IInterpreterLocatorProgressService, IInterpreterLocatorService, PythonInterpreter } from '../contracts'; - -@injectable() -export class InterpreterLocatorProgressService implements IInterpreterLocatorProgressService { - private deferreds: Deferred<PythonInterpreter[]>[] = []; - private readonly refreshing = new EventEmitter<void>(); - private readonly refreshed = new EventEmitter<void>(); - private readonly locators: IInterpreterLocatorService[] = []; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IDisposableRegistry) private readonly disposables: Disposable[]) { - this.locators = serviceContainer.getAll<IInterpreterLocatorService>(IInterpreterLocatorService); - } - - public get onRefreshing(): Event<void> { - return this.refreshing.event; - } - public get onRefreshed(): Event<void> { - 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<PythonInterpreter[]>) { - 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(); - } - @traceDecorators.verbose('Checking whether locactors have completed locating') - private checkProgress() { - if (this.areAllItemsCcomplete()) { - return this.notifyCompleted(); - } - Promise.all(this.deferreds.map(item => item.promise)) - .catch(noop) - .then(() => this.checkProgress()) - .ignoreErrors(); - } - private areAllItemsCcomplete() { - this.deferreds = this.deferreds.filter(item => !item.completed); - return this.deferreds.length === 0; - } -} diff --git a/src/client/interpreter/locators/services/KnownPathsService.ts b/src/client/interpreter/locators/services/KnownPathsService.ts deleted file mode 100644 index b4328186c5a6..000000000000 --- a/src/client/interpreter/locators/services/KnownPathsService.ts +++ /dev/null @@ -1,111 +0,0 @@ -// tslint:disable:no-require-imports no-var-requires no-unnecessary-callback-wrapper -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { IPlatformService } from '../../../common/platform/types'; -import { ICurrentProcess, IPathUtils } from '../../../common/types'; -import { fsExistsAsync } from '../../../common/utils/fs'; -import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterHelper, IKnownSearchPathsForInterpreters, InterpreterType, PythonInterpreter } from '../../contracts'; -import { lookForInterpretersInDirectory } from '../helpers'; -import { CacheableLocatorService } from './cacheableLocatorService'; -const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); - -/** - * Locates "known" paths. - */ -@injectable() -export class KnownPathsService extends CacheableLocatorService { - public constructor( - @inject(IKnownSearchPathsForInterpreters) private knownSearchPaths: IKnownSearchPathsForInterpreters, - @inject(IInterpreterHelper) private helper: IInterpreterHelper, - @inject(IServiceContainer) serviceContainer: IServiceContainer - ) { - super('KnownPathsService', serviceContainer); - } - - /** - * Release any held resources. - * - * Called by VS Code to indicate it is done with the resource. - */ - // tslint:disable-next-line:no-empty - public dispose() { } - - /** - * Return the located interpreters. - * - * This is used by CacheableLocatorService.getInterpreters(). - */ - protected getInterpretersImplementation(resource?: Uri): Promise<PythonInterpreter[]> { - return this.suggestionsFromKnownPaths(); - } - - /** - * Return the located interpreters. - */ - private suggestionsFromKnownPaths() { - const promises = this.knownSearchPaths.getSearchPaths().map(dir => this.getInterpretersInDirectory(dir)); - return Promise.all<string[]>(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) { - return fsExistsAsync(dir) - .then(exists => exists ? lookForInterpretersInDirectory(dir) : Promise.resolve<string[]>([])); - } -} - -@injectable() -export class KnownSearchPathsForInterpreters implements IKnownSearchPathsForInterpreters { - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { } - /** - * Return the paths where Python interpreters might be found. - */ - public getSearchPaths(): string[] { - const currentProcess = this.serviceContainer.get<ICurrentProcess>(ICurrentProcess); - const platformService = this.serviceContainer.get<IPlatformService>(IPlatformService); - const pathUtils = this.serviceContainer.get<IPathUtils>(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/interpreter/locators/services/baseVirtualEnvService.ts b/src/client/interpreter/locators/services/baseVirtualEnvService.ts deleted file mode 100644 index 22a356312d50..000000000000 --- a/src/client/interpreter/locators/services/baseVirtualEnvService.ts +++ /dev/null @@ -1,88 +0,0 @@ -// tslint:disable:no-unnecessary-callback-wrapper no-require-imports no-var-requires - -import { injectable, unmanaged } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { IFileSystem, IPlatformService } from '../../../common/platform/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterHelper, IVirtualEnvironmentsSearchPathProvider, PythonInterpreter } from '../../contracts'; -import { IVirtualEnvironmentManager } from '../../virtualEnvs/types'; -import { lookForInterpretersInDirectory } from '../helpers'; -import { CacheableLocatorService } from './cacheableLocatorService'; -const flatten = require('lodash/flatten') as typeof import('lodash/flatten'); - -@injectable() -export class BaseVirtualEnvService extends CacheableLocatorService { - private readonly virtualEnvMgr: IVirtualEnvironmentManager; - private readonly helper: IInterpreterHelper; - private readonly fileSystem: IFileSystem; - public constructor(@unmanaged() private searchPathsProvider: IVirtualEnvironmentsSearchPathProvider, - @unmanaged() serviceContainer: IServiceContainer, - @unmanaged() name: string, - @unmanaged() cachePerWorkspace: boolean = false) { - super(name, serviceContainer, cachePerWorkspace); - this.virtualEnvMgr = serviceContainer.get<IVirtualEnvironmentManager>(IVirtualEnvironmentManager); - this.helper = serviceContainer.get<IInterpreterHelper>(IInterpreterHelper); - this.fileSystem = serviceContainer.get<IFileSystem>(IFileSystem); - } - // tslint:disable-next-line:no-empty - public dispose() { } - protected getInterpretersImplementation(resource?: Uri): Promise<PythonInterpreter[]> { - 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(lookForInterpretersInDirectory))) - .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) => { - console.error('Python Extension (lookForInterpretersInVenvs):', err); - // Ignore exceptions. - return [] as PythonInterpreter[]; - }); - } - private getProspectiveDirectoriesForLookup(subDirs: string[]) { - const platform = this.serviceContainer.get<IPlatformService>(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) => { - console.error('Python Extension (getProspectiveDirectoriesForLookup):', err); - // Ignore exceptions. - return ''; - })); - } - private async getVirtualEnvDetails(interpreter: string, resource?: Uri): Promise<PythonInterpreter | undefined> { - 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 - }; - }); - } -} diff --git a/src/client/interpreter/locators/services/cacheableLocatorService.ts b/src/client/interpreter/locators/services/cacheableLocatorService.ts deleted file mode 100644 index 395d3e0929da..000000000000 --- a/src/client/interpreter/locators/services/cacheableLocatorService.ts +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// tslint:disable:no-any - -import { injectable, unmanaged } from 'inversify'; -import * as md5 from 'md5'; -import { Disposable, Event, EventEmitter, Uri } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import '../../../common/extensions'; -import { Logger } from '../../../common/logger'; -import { IDisposableRegistry, IPersistentStateFactory } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; -import { IServiceContainer } from '../../../ioc/types'; -import { sendTelemetryWhenDone } from '../../../telemetry'; -import { PYTHON_INTERPRETER_DISCOVERY } from '../../../telemetry/constants'; -import { IInterpreterLocatorService, IInterpreterWatcher, PythonInterpreter } from '../../contracts'; - -@injectable() -export abstract class CacheableLocatorService implements IInterpreterLocatorService { - protected readonly _hasInterpreters: Deferred<boolean>; - private readonly promisesPerResource = new Map<string, Deferred<PythonInterpreter[]>>(); - private readonly handlersAddedToResource = new Set<string>(); - private readonly cacheKeyPrefix: string; - private readonly locating = new EventEmitter<Promise<PythonInterpreter[]>>(); - constructor(@unmanaged() private readonly name: string, - @unmanaged() protected readonly serviceContainer: IServiceContainer, - @unmanaged() private cachePerWorkspace: boolean = false) { - this._hasInterpreters = createDeferred<boolean>(); - this.cacheKeyPrefix = `INTERPRETERS_CACHE_v3_${name}`; - } - public get onLocating(): Event<Promise<PythonInterpreter[]>> { - return this.locating.event; - } - public get hasInterpreters(): Promise<boolean> { - return this._hasInterpreters.promise; - } - public abstract dispose(); - public async getInterpreters(resource?: Uri, ignoreCache?: boolean): Promise<PythonInterpreter[]> { - const cacheKey = this.getCacheKey(resource); - let deferred = this.promisesPerResource.get(cacheKey); - - if (!deferred || ignoreCache) { - deferred = createDeferred<PythonInterpreter[]>(); - this.promisesPerResource.set(cacheKey, deferred); - - this.addHandlersForInterpreterWatchers(cacheKey, resource) - .ignoreErrors(); - - const promise = this.getInterpretersImplementation(resource) - .then(async items => { - await this.cacheInterpreters(items, resource); - deferred!.resolve(items); - }) - .catch(ex => deferred!.reject(ex)); - - sendTelemetryWhenDone(PYTHON_INTERPRETER_DISCOVERY, promise, undefined, { locator: this.name }); - 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 = this.getCachedInterpreters(resource); - return Array.isArray(cachedInterpreters) ? cachedInterpreters : deferred.promise; - } - protected async addHandlersForInterpreterWatchers(cacheKey: string, resource: Uri | undefined): Promise<void> { - if (this.handlersAddedToResource.has(cacheKey)) { - return; - } - this.handlersAddedToResource.add(cacheKey); - const watchers = await this.getInterpreterWatchers(resource); - const disposableRegisry = this.serviceContainer.get<Disposable[]>(IDisposableRegistry); - watchers.forEach(watcher => { - watcher.onDidCreate(() => { - Logger.verbose(`Interpreter Watcher change handler for ${this.cacheKeyPrefix}`); - this.promisesPerResource.delete(cacheKey); - this.getInterpreters(resource).ignoreErrors(); - }, this, disposableRegisry); - }); - } - protected async getInterpreterWatchers(_resource: Uri | undefined): Promise<IInterpreterWatcher[]> { - return []; - } - - protected abstract getInterpretersImplementation(resource?: Uri): Promise<PythonInterpreter[]>; - protected createPersistenceStore(resource?: Uri) { - const cacheKey = this.getCacheKey(resource); - const persistentFactory = this.serviceContainer.get<IPersistentStateFactory>(IPersistentStateFactory); - if (this.cachePerWorkspace) { - return persistentFactory.createWorkspacePersistentState<PythonInterpreter[]>(cacheKey, undefined as any); - } else { - return persistentFactory.createGlobalPersistentState<PythonInterpreter[]>(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>(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/interpreter/locators/services/conda.ts b/src/client/interpreter/locators/services/conda.ts deleted file mode 100644 index 1f25682b4f6d..000000000000 --- a/src/client/interpreter/locators/services/conda.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 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 AnacondaIdentfiers = ['Anaconda', 'Conda', 'Continuum']; diff --git a/src/client/interpreter/locators/services/condaEnvFileService.ts b/src/client/interpreter/locators/services/condaEnvFileService.ts deleted file mode 100644 index 196f9ba045d8..000000000000 --- a/src/client/interpreter/locators/services/condaEnvFileService.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { IFileSystem } from '../../../common/platform/types'; -import { ILogger } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { - ICondaService, - IInterpreterHelper, - InterpreterType, - PythonInterpreter -} from '../../contracts'; -import { CacheableLocatorService } from './cacheableLocatorService'; -import { AnacondaCompanyName } from './conda'; - -/** - * Locate conda env interpreters based on the "conda environments file". - */ -@injectable() -export class CondaEnvFileService extends CacheableLocatorService { - constructor( - @inject(IInterpreterHelper) private helperService: IInterpreterHelper, - @inject(ICondaService) private condaService: ICondaService, - @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(ILogger) private logger: ILogger - ) { - 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<PythonInterpreter[]> { - return this.getSuggestionsFromConda(); - } - - /** - * Return the list of interpreters identified by the "conda environments file". - */ - private async getSuggestionsFromConda(): Promise<PythonInterpreter[]> { - 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) { - this.logger.logError('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<PythonInterpreter | undefined> { - 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/interpreter/locators/services/condaEnvService.ts b/src/client/interpreter/locators/services/condaEnvService.ts deleted file mode 100644 index ffc0c32fd5fc..000000000000 --- a/src/client/interpreter/locators/services/condaEnvService.ts +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { IFileSystem } from '../../../common/platform/types'; -import { ILogger } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { CondaInfo, ICondaService, IInterpreterHelper, InterpreterType, PythonInterpreter } from '../../contracts'; -import { CacheableLocatorService } from './cacheableLocatorService'; -import { AnacondaCompanyName } from './conda'; - -/** - * Locates conda env interpreters based on the conda service's info. - */ -@injectable() -export class CondaEnvService extends CacheableLocatorService { - - constructor( - @inject(ICondaService) private condaService: ICondaService, - @inject(IInterpreterHelper) private helper: IInterpreterHelper, - @inject(ILogger) private logger: ILogger, - @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<PythonInterpreter[]> { - return this.getSuggestionsFromConda(); - } - - /** - * Return the list of interpreters for all the conda envs. - */ - private async getSuggestionsFromConda(): Promise<PythonInterpreter[]> { - try { - const info = await this.condaService.getCondaInfo(); - if (!info) { - return []; - } - const interpreters = await parseCondaInfo( - info, - this.condaService, - this.fileSystem, - this.helper - ); - 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. - this.logger.logError('Failed to get Suggestions from conda', ex); - return []; - } - } -} - -/** - * Return the list of conda env interpreters. - */ -export async function parseCondaInfo( - info: CondaInfo, - condaService: ICondaService, - fileSystem: IFileSystem, - helper: IInterpreterHelper -) { - // 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 = condaService.getInterpreterPath(envPath); - - if (!(await fileSystem.fileExists(pythonPath))) { - return; - } - const details = await helper.getInterpreterInformation(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/interpreter/locators/services/condaHelper.ts b/src/client/interpreter/locators/services/condaHelper.ts deleted file mode 100644 index 4d7c4c1fd1b3..000000000000 --- a/src/client/interpreter/locators/services/condaHelper.ts +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as path from 'path'; -import '../../../common/extensions'; -import { CondaInfo } from '../../contracts'; -import { AnacondaDisplayName, AnacondaIdentfiers } from './conda'; - -export type EnvironmentPath = string; -export type EnvironmentName = string; - -/** - * Helpers for conda. - */ -export class CondaHelper { - - /** - * Return the string to display for the conda interpreter. - */ - public 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 (this.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 - * 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 - * @param {string} condaEnvironmentList - * @param {CondaInfo} condaInfo - * @returns {{ name: string, path: string }[] | undefined} - * @memberof CondaHelper - */ - public parseCondaEnvironmentNames(condaEnvironmentList: string): { name: string; path: string }[] | undefined { - const environments = condaEnvironmentList.splitLines({ trim: false }); - const baseEnvironmentLine = environments.filter(line => line.indexOf('*') > 0); - if (baseEnvironmentLine.length === 0) { - return; - } - const pathStartIndex = baseEnvironmentLine[0].indexOf(baseEnvironmentLine[0].split('*')[1].trim()); - const envs: { name: string; path: string }[] = []; - environments.forEach(line => { - if (line.length <= pathStartIndex) { - return; - } - let name = line.substring(0, pathStartIndex).trim(); - if (name.endsWith('*')) { - name = name.substring(0, name.length - 1).trim(); - } - const envPath = line.substring(pathStartIndex).trim(); - name = name.length === 0 ? path.basename(envPath) : name; - if (name.length > 0 && envPath.length > 0) { - envs.push({ name, path: envPath }); - } - }); - - return envs; - } - - /** - * Does the given string match a known Anaconda identifier. - */ - private isIdentifiableAsAnaconda(value: string) { - const valueToSearch = value.toLowerCase(); - return AnacondaIdentfiers.some(item => valueToSearch.indexOf(item.toLowerCase()) !== -1); - } -} diff --git a/src/client/interpreter/locators/services/condaService.ts b/src/client/interpreter/locators/services/condaService.ts deleted file mode 100644 index b752a7a355d7..000000000000 --- a/src/client/interpreter/locators/services/condaService.ts +++ /dev/null @@ -1,507 +0,0 @@ -import { inject, injectable, named, optional } from 'inversify'; -import * as path from 'path'; -import { compare, parse, SemVer } from 'semver'; -import { ConfigurationChangeEvent, Uri } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import { Logger } from '../../../common/logger'; -import { IFileSystem, IPlatformService } from '../../../common/platform/types'; -import { ExecutionResult, IProcessServiceFactory } from '../../../common/process/types'; -import { ITerminalActivationCommandProvider, TerminalShellType } from '../../../common/terminal/types'; -import { IConfigurationService, IDisposableRegistry, ILogger, IPersistentStateFactory } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { - CondaInfo, - ICondaService, - IInterpreterLocatorService, - IInterpreterService, - InterpreterType, - PythonInterpreter, - WINDOWS_REGISTRY_SERVICE -} from '../../contracts'; -import { CondaHelper } 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 = [ - '/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(',')}}`; - -// Regex for splitting environment strings -const EnvironmentSplitRegex = /^\s*([^=]+)\s*=\s*(.+)\s*$/; - -export const CondaGetEnvironmentPrefix = 'Outputting Environment Now...'; - -/** - * A wrapper around a conda installation. - */ -@injectable() -export class CondaService implements ICondaService { - private condaFile?: Promise<string | undefined>; - private isAvailable: boolean | undefined; - private readonly condaHelper = new CondaHelper(); - private activatedEnvironmentCache: { [key: string]: NodeJS.ProcessEnv } = {}; - private activationProvider: ITerminalActivationCommandProvider; - private shellType: TerminalShellType; - - 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(ILogger) private logger: ILogger, - @inject(IInterpreterService) private interpreterService: IInterpreterService, - @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(IInterpreterLocatorService) @named(WINDOWS_REGISTRY_SERVICE) @optional() private registryLookupForConda?: IInterpreterLocatorService - ) { - this.disposableRegistry.push(this.interpreterService.onDidChangeInterpreter(this.onInterpreterChanged.bind(this))); - this.activationProvider = serviceContainer.get<ITerminalActivationCommandProvider>(ITerminalActivationCommandProvider, - this.platform.isWindows ? 'commandPromptAndPowerShell' : 'bashCShellFish'); - this.shellType = this.platform.isWindows ? TerminalShellType.commandPrompt : TerminalShellType.bash; // Defaults for Child_Process.exec - 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<string> { - 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<boolean> { - 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. - */ - public async getCondaVersion(): Promise<SemVer | undefined> { - const processService = await this.processServiceFactory.create(); - const info = await this.getCondaInfo().catch<CondaInfo | undefined>(() => 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<string | undefined>(() => 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. - Logger.warn(`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. - */ - public async getCondaInfo(): Promise<CondaInfo | undefined> { - 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<boolean>} - * @memberof CondaService - */ - public async isCondaEnvironment(interpreterPath: string): Promise<boolean> { - 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). - */ - public async getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string; path: string }[]) | undefined> { - // Global cache. - // tslint:disable-next-line:no-any - const globalPersistence = this.persistentStateFactory.createGlobalPersistentState<{ data: { name: string; path: string }[] | undefined }>('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(); - const envInfo = await processService.exec(condaFile, ['env', 'list']).then(output => output.stdout); - const environments = this.condaHelper.parseCondaEnvironmentNames(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. - this.logger.logInformation('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); - } - - /** - * For the given interpreter return an activated Conda environment object - * with the correct addition to the path and environmental variables - */ - public getActivatedCondaEnvironment = async (interpreter: PythonInterpreter, inputEnvironment?: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv> => { - const input = inputEnvironment ? inputEnvironment : process.env; - if (interpreter.type !== InterpreterType.Conda) { - return input; - } - - // Shell execute conda activate and scrape the environment from it. This should be the necessary environment to - // run anything that depends upon conda - const condaEnvironmentName = interpreter.envName ? interpreter.envName : interpreter.path; - - // We may have already computed this cache on a previous request - if (this.activatedEnvironmentCache && - this.activatedEnvironmentCache.hasOwnProperty(condaEnvironmentName)) { - return this.activatedEnvironmentCache[condaEnvironmentName]; - } - - // New environment - - // Attempt to find where conda is installed. - const condaPath = await this.getCondaFileFromInterpreter(interpreter); - if (!condaPath) { - return input; - } - - // From that path we need to start an activate script - const activateCommands = this.activationProvider.getActivationCommandsForInterpreter ? - await this.activationProvider.getActivationCommandsForInterpreter(condaPath, this.shellType) : - this.platform.isWindows ? - [`"${path.join(path.dirname(condaPath), 'activate')}"`] : - [`. "${path.join(path.dirname(condaPath), 'activate')}"`]; - - const result = { ...input }; - const processService = await this.processServiceFactory.create(); - - // Run the activate command collect the environment from it. - const listEnv = this.platform.isWindows ? 'set' : 'printenv'; - let shellExecResult: ExecutionResult<string> | undefined; - - for (let i = 0; activateCommands && i < activateCommands.length && !shellExecResult; i += 1) { - // Replace 'source ' with '. ' as that works in shell exec - const activateCommand = activateCommands[i].replace(/^source\s+/, '. '); - - // tslint:disable-next-line:no-any - let error: any; - try { - // In order to make sure we know where the environment output is, - // put in a dummy echo we can look for - const command = `${activateCommand} && conda activate ${condaEnvironmentName} && echo '${CondaGetEnvironmentPrefix}' && ${listEnv}`; - shellExecResult = await processService.shellExec(command, { env: inputEnvironment }); - } catch (err) { - // If that crashes for whatever reason, then just return empty data. - this.logger.logWarning(err); - error = err; - } - - // Special case. The 'environment' we have is the base environment. Previous call would have - // thrown an error. - if (!shellExecResult && error) { - try { - const command = `"${activateCommand}" && echo '${CondaGetEnvironmentPrefix}' && ${listEnv}`; - shellExecResult = await processService.shellExec(command, { env: inputEnvironment }); - } catch (err) { - // If that crashes for whatever reason, then just return empty data. - this.logger.logWarning(err); - } - } - } - - // Parse the lines of the output until we find the dummy command - if (shellExecResult && shellExecResult.stdout.length > 0) { - this.parseEnvironmentOutput(shellExecResult.stdout, result); - } else { - // Still not found. Try just adding some things by hand. - this.addDefaultCondaEnvironment(interpreter, result); - } - - this.activatedEnvironmentCache[condaEnvironmentName] = result; - return this.activatedEnvironmentCache[condaEnvironmentName]; - } - - private parseEnvironmentOutput(output: string, result: NodeJS.ProcessEnv) { - const lines = output.splitLines({ trim: true, removeEmptyEntries: true }); - let foundDummyOutput = false; - for (let i = 0; i < lines.length; i += 1) { - if (foundDummyOutput) { - // Split on equal - const match = EnvironmentSplitRegex.exec(lines[i]); - if (match && match !== null && match.length > 2) { - result[match[1]] = match[2]; - } - } else { - // See if we found the dummy output or not yet - foundDummyOutput = lines[i].includes(CondaGetEnvironmentPrefix); - } - } - } - - /** - * Adds the default paths and env vars for conda to the current result - */ - private addDefaultCondaEnvironment(interpreter: PythonInterpreter, result: NodeJS.ProcessEnv) { - if (interpreter.envPath) { - if (this.platform.isWindows) { - // Windows: Path, ; as separator, 'Scripts' as directory - const condaPath = path.join(interpreter.envPath, 'Scripts'); - result.Path = condaPath.concat(';', `${result.Path ? result.Path : ''}`); - } else { - // Mac: PATH, : as separator, 'bin' as directory - const condaPath = path.join(interpreter.envPath, 'bin'); - result.PATH = condaPath.concat(':', `${result.PATH ? result.PATH : ''}`); - } - - // Conda also wants a couple of environmental variables set - result.CONDA_PREFIX = interpreter.envPath; - } - - if (interpreter.envName) { - result.CONDA_DEFAULT_ENV = interpreter.envName; - } - } - - /** - * 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 async getCondaFileFromInterpreter(interpreter: PythonInterpreter | undefined): Promise<string | undefined> { - const condaExe = this.platform.isWindows ? 'conda.exe' : 'conda'; - const scriptsDir = this.platform.isWindows ? 'Scripts' : 'bin'; - const interpreterDir = interpreter ? path.dirname(interpreter.path) : ''; - const envName = interpreter && interpreter.envName ? interpreter.envName : undefined; - let condaPath = path.join(interpreterDir, condaExe); - if (await this.fileSystem.fileExists(condaPath)) { - return condaPath; - } - // Conda path has changed locations, check the new location in the scripts directory after checking - // the old location - condaPath = path.join(interpreterDir, scriptsDir, condaExe); - if (await this.fileSystem.fileExists(condaPath)) { - return condaPath; - } - - // 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); - condaPath = path.join(originalPath, condaExe); - - if (await this.fileSystem.fileExists(condaPath)) { - return condaPath; - } - - // Also look in the scripts directory here too. - condaPath = path.join(originalPath, scriptsDir, condaExe); - if (await this.fileSystem.fileExists(condaPath)) { - return condaPath; - } - } - } - - 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); - const interpreterPath = await this.getCondaFileFromInterpreter(condaInterpreter); - 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<string> { - const globPattern = this.platform.isWindows ? CondaLocationsGlobWin : CondaLocationsGlob; - const condaFiles = await this.fileSystem.search(globPattern) - .catch<string[]>((failReason) => { - Logger.warn( - '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]; - } - - /** - * Called when the user changes the current interpreter. - */ - private onInterpreterChanged(): void { - // Clear our activated environment cache as it can't match the current one anymore - this.activatedEnvironmentCache = {}; - } -} diff --git a/src/client/interpreter/locators/services/currentPathService.ts b/src/client/interpreter/locators/services/currentPathService.ts deleted file mode 100644 index ee2f3be47a39..000000000000 --- a/src/client/interpreter/locators/services/currentPathService.ts +++ /dev/null @@ -1,126 +0,0 @@ -// tslint:disable:no-require-imports no-var-requires underscore-consistent-invocation no-unnecessary-callback-wrapper -import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { traceError, traceInfo } from '../../../common/logger'; -import { IFileSystem, IPlatformService } from '../../../common/platform/types'; -import { IProcessServiceFactory } from '../../../common/process/types'; -import { IConfigurationService } from '../../../common/types'; -import { OSType } from '../../../common/utils/platform'; -import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterHelper, InterpreterType, PythonInterpreter } from '../../contracts'; -import { IPythonInPathCommandProvider } from '../types'; -import { CacheableLocatorService } from './cacheableLocatorService'; - -/** - * Locates the currently configured Python interpreter. - * - * If no interpreter is configured then it falls back to the system - * Python (3 then 2). - */ -@injectable() -export class CurrentPathService extends CacheableLocatorService { - private readonly fs: IFileSystem; - - public constructor( - @inject(IInterpreterHelper) private helper: IInterpreterHelper, - @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, - @inject(IPythonInPathCommandProvider) private readonly pythonCommandProvider: IPythonInPathCommandProvider, - @inject(IServiceContainer) serviceContainer: IServiceContainer - ) { - super('CurrentPathService', serviceContainer); - this.fs = serviceContainer.get<IFileSystem>(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<PythonInterpreter[]> { - return this.suggestionsFromKnownPaths(resource); - } - - /** - * Return the located interpreters. - */ - private async suggestionsFromKnownPaths(resource?: Uri) { - const configSettings = this.serviceContainer.get<IConfigurationService>(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<PythonInterpreter | undefined> { - 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 args = Array.isArray(options.args) ? options.args : []; - return processService.exec(options.command, args.concat(['-c', 'import sys;print(sys.executable)']), {}) - .then(output => output.stdout.trim()) - .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 ${args.join(' ')} failed as file ${value} does not exist`); - return ''; - }) - .catch(ex => { - traceInfo(`Detection of Python Interpreter for Command ${options.command} and args ${args.join(' ')} failed`); - return ''; - }); // Ignore exceptions in getting the executable. - } catch (ex) { - traceError(`Detection of Python Interpreter for Command ${options.command} failed`, ex); - return ''; // Ignore exceptions in getting the executable. - } - } -} - -@injectable() -export class PythonInPathCommandProvider implements IPythonInPathCommandProvider { - constructor(@inject(IPlatformService) private readonly platform: IPlatformService) { } - public getCommands(): { command: string; args?: string[] }[] { - const paths = ['python3.7', 'python3.6', 'python3', 'python2', 'python'] - .map(item => { 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/interpreter/locators/services/globalVirtualEnvService.ts b/src/client/interpreter/locators/services/globalVirtualEnvService.ts deleted file mode 100644 index 9dd0b827f85e..000000000000 --- a/src/client/interpreter/locators/services/globalVirtualEnvService.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 * as os from 'os'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { IConfigurationService } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { IVirtualEnvironmentsSearchPathProvider } from '../../contracts'; -import { IVirtualEnvironmentManager } from '../../virtualEnvs/types'; -import { BaseVirtualEnvService } from './baseVirtualEnvService'; - -@injectable() -export class GlobalVirtualEnvService extends BaseVirtualEnvService { - public constructor( - @inject(IVirtualEnvironmentsSearchPathProvider) @named('global') globalVirtualEnvPathProvider: IVirtualEnvironmentsSearchPathProvider, - @inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(globalVirtualEnvPathProvider, serviceContainer, 'VirtualEnvService'); - } -} - -@injectable() -export class GlobalVirtualEnvironmentsSearchPathProvider implements IVirtualEnvironmentsSearchPathProvider { - private readonly config: IConfigurationService; - private readonly virtualEnvMgr: IVirtualEnvironmentManager; - - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.config = serviceContainer.get<IConfigurationService>(IConfigurationService); - this.virtualEnvMgr = serviceContainer.get<IVirtualEnvironmentManager>(IVirtualEnvironmentManager); - } - - public async getSearchPaths(resource?: Uri): Promise<string[]> { - const homedir = os.homedir(); - const venvFolders = this.config.getSettings(resource).venvFolders; - const folders = venvFolders.map(item => path.join(homedir, item)); - - // 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/interpreter/locators/services/interpreterWatcherBuilder.ts b/src/client/interpreter/locators/services/interpreterWatcherBuilder.ts deleted file mode 100644 index e559efd0b897..000000000000 --- a/src/client/interpreter/locators/services/interpreterWatcherBuilder.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 { Uri } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import { traceDecorators } from '../../../common/logger'; -import { createDeferred } from '../../../common/utils/async'; -import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterWatcher, IInterpreterWatcherBuilder, WORKSPACE_VIRTUAL_ENV_SERVICE } from '../../contracts'; -import { WorkspaceVirtualEnvWatcherService } from './workspaceVirtualEnvWatcherService'; - -@injectable() -export class InterpreterWatcherBuilder implements IInterpreterWatcherBuilder { - private readonly watchersByResource = new Map<string, Promise<IInterpreterWatcher>>(); - /** - * 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<IInterpreterWatcher> { - const key = this.getResourceKey(resource); - if (!this.watchersByResource.has(key)) { - const deferred = createDeferred<IInterpreterWatcher>(); - this.watchersByResource.set(key, deferred.promise); - const watcher = this.serviceContainer.get<WorkspaceVirtualEnvWatcherService>(IInterpreterWatcher, WORKSPACE_VIRTUAL_ENV_SERVICE); - await watcher.register(resource); - deferred.resolve(watcher); - } - return this.watchersByResource.get(key)!; - } - protected getResourceKey(resource: Uri | undefined): string { - const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; - return workspaceFolder ? workspaceFolder.uri.fsPath : ''; - } -} diff --git a/src/client/interpreter/locators/services/pipEnvService.ts b/src/client/interpreter/locators/services/pipEnvService.ts deleted file mode 100644 index ffd55ff4f1b7..000000000000 --- a/src/client/interpreter/locators/services/pipEnvService.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 { IApplicationShell, IWorkspaceService } from '../../../common/application/types'; -import { traceError } from '../../../common/logger'; -import { IFileSystem, IPlatformService } from '../../../common/platform/types'; -import { IProcessServiceFactory } from '../../../common/process/types'; -import { ICurrentProcess, ILogger } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterHelper, InterpreterType, IPipEnvService, PythonInterpreter } from '../../contracts'; -import { CacheableLocatorService } from './cacheableLocatorService'; - -const execName = 'pipenv'; -const pipEnvFileNameVariable = 'PIPENV_PIPFILE'; - -@injectable() -export class PipEnvService extends CacheableLocatorService implements IPipEnvService { - private readonly helper: IInterpreterHelper; - private readonly processServiceFactory: IProcessServiceFactory; - private readonly workspace: IWorkspaceService; - private readonly fs: IFileSystem; - private readonly logger: ILogger; - - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super('PipEnvService', serviceContainer); - this.helper = this.serviceContainer.get<IInterpreterHelper>(IInterpreterHelper); - this.processServiceFactory = this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory); - this.workspace = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem); - this.logger = this.serviceContainer.get<ILogger>(ILogger); - } - // tslint:disable-next-line:no-empty - public dispose() { } - public async isRelatedPipEnvironment(dir: string, pythonPath: string): Promise<boolean> { - // 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; - } - protected getInterpretersImplementation(resource?: Uri): Promise<PythonInterpreter[]> { - 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<PythonInterpreter | undefined> { - const interpreterPath = await this.getInterpreterPathFromPipenv(pipenvCwd); - if (!interpreterPath) { - return; - } - - const details = await this.helper.getInterpreterInformation(interpreterPath); - if (!details) { - return; - } - this._hasInterpreters.resolve(true); - return { - ...(details as PythonInterpreter), - path: interpreterPath, - type: InterpreterType.PipEnv - }; - } - - 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<string | undefined> { - // Quick check before actually running pipenv - if (!await this.checkIfPipFileExists(cwd)) { - return; - } - try { - 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; - } - const errorMessage = error.message || error; - const appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell); - appShell.showWarningMessage(`Workspace contains pipfile but attempt to run 'pipenv --py' failed with ${errorMessage}. Make sure pipenv is on the PATH.`); - } - } - private async checkIfPipFileExists(cwd: string): Promise<boolean> { - const currentProcess = this.serviceContainer.get<ICurrentProcess>(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<string | undefined> { - try { - const processService = await this.processServiceFactory.create(Uri.file(rootPath)); - 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>(IPlatformService); - const currentProc = this.serviceContainer.get<ICurrentProcess>(ICurrentProcess); - const enviromentVariableValues = { - LC_ALL: currentProc.env.LC_ALL, - LANG: currentProc.env.LANG - }; - enviromentVariableValues[platformService.pathVariableName] = currentProc.env[platformService.pathVariableName]; - - this.logger.logWarning('Error in invoking PipEnv', error); - this.logger.logWarning(`Relevant Environment Variables ${JSON.stringify(enviromentVariableValues, undefined, 4)}`); - const errorMessage = error.message || error; - const appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell); - appShell.showWarningMessage(`Workspace contains pipfile but attempt to run 'pipenv --venv' failed with '${errorMessage}'. Make sure pipenv is on the PATH.`); - } - } -} diff --git a/src/client/interpreter/locators/services/windowsRegistryService.ts b/src/client/interpreter/locators/services/windowsRegistryService.ts deleted file mode 100644 index 0c6e842f4f3b..000000000000 --- a/src/client/interpreter/locators/services/windowsRegistryService.ts +++ /dev/null @@ -1,154 +0,0 @@ -// tslint:disable:no-require-imports no-var-requires underscore-consistent-invocation -import * as fs from 'fs-extra'; -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { IPlatformService, IRegistry, RegistryHive } from '../../../common/platform/types'; -import { IPathUtils } from '../../../common/types'; -import { Architecture } from '../../../common/utils/platform'; -import { parsePythonVersion } from '../../../common/utils/version'; -import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterHelper, InterpreterType, PythonInterpreter } from '../../contracts'; -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; -}; - -@injectable() -export class WindowsRegistryService extends CacheableLocatorService { - private readonly pathUtils: IPathUtils; - constructor(@inject(IRegistry) private registry: IRegistry, - @inject(IPlatformService) private readonly platform: IPlatformService, - @inject(IServiceContainer) serviceContainer: IServiceContainer) { - super('WindowsRegistryService', serviceContainer); - this.pathUtils = serviceContainer.get<IPathUtils>(IPathUtils); - } - // tslint:disable-next-line:no-empty - public dispose() { } - protected async getInterpretersImplementation(_resource?: Uri): Promise<PythonInterpreter[]> { - 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<CompanyInterpreter[]>[] = [ - 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<CompanyInterpreter[]>(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<PythonInterpreter[]>((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<CompanyInterpreter[]> { - 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<PythonInterpreter | undefined | null> { - 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); - const helper = this.serviceContainer.get<IInterpreterHelper>(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, - version: parsePythonVersion(version), - companyDisplayName: interpreterInfo.companyDisplayName, - type: InterpreterType.Unknown - } as PythonInterpreter; - }) - .then(interpreter => interpreter ? fs.pathExists(interpreter.path).catch(() => false).then(exists => exists ? interpreter : null) : null) - .catch(error => { - console.error(`Failed to retrieve interpreter details for company ${companyKey},tag: ${tagKey}, hive: ${hive}, arch: ${arch}`); - console.error(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/interpreter/locators/services/workspaceVirtualEnvService.ts b/src/client/interpreter/locators/services/workspaceVirtualEnvService.ts deleted file mode 100644 index 05ccfe3a0d18..000000000000 --- a/src/client/interpreter/locators/services/workspaceVirtualEnvService.ts +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-require-imports - -import { inject, injectable, named } from 'inversify'; -import * as path from 'path'; -import untildify = require('untildify'); -import { Uri } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import { IConfigurationService } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterWatcher, IInterpreterWatcherBuilder, IVirtualEnvironmentsSearchPathProvider } from '../../contracts'; -import { BaseVirtualEnvService } from './baseVirtualEnvService'; - -@injectable() -export class WorkspaceVirtualEnvService extends BaseVirtualEnvService { - public constructor( - @inject(IVirtualEnvironmentsSearchPathProvider) @named('workspace') workspaceVirtualEnvPathProvider: IVirtualEnvironmentsSearchPathProvider, - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IInterpreterWatcherBuilder) private readonly builder: IInterpreterWatcherBuilder) { - super(workspaceVirtualEnvPathProvider, serviceContainer, 'WorkspaceVirtualEnvService', true); - } - protected async getInterpreterWatchers(resource: Uri | undefined): Promise<IInterpreterWatcher[]> { - return [await this.builder.getWorkspaceVirtualEnvInterpreterWatcher(resource)]; - } -} - -@injectable() -export class WorkspaceVirtualEnvironmentsSearchPathProvider implements IVirtualEnvironmentsSearchPathProvider { - public constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - - } - public async getSearchPaths(resource?: Uri): Promise<string[]> { - const configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); - const paths: string[] = []; - const venvPath = configService.getSettings(resource).venvPath; - if (venvPath) { - paths.push(untildify(venvPath)); - } - const workspaceService = this.serviceContainer.get<IWorkspaceService>(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/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts b/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts deleted file mode 100644 index 30d948e2b5e8..000000000000 --- a/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts +++ /dev/null @@ -1,94 +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 { Disposable, Event, EventEmitter, FileSystemWatcher, RelativePattern, Uri } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import '../../../common/extensions'; -import { Logger, traceDecorators } from '../../../common/logger'; -import { IPlatformService } from '../../../common/platform/types'; -import { IPythonExecutionFactory } from '../../../common/process/types'; -import { IDisposableRegistry } from '../../../common/types'; -import { IInterpreterWatcher } from '../../contracts'; - -const maxTimeToWaitForEnvCreation = 60_000; -const timeToPollForEnvCreation = 2_000; - -@injectable() -export class WorkspaceVirtualEnvWatcherService implements IInterpreterWatcher, Disposable { - private readonly didCreate: EventEmitter<void>; - private timers = new Map<string, { timer: NodeJS.Timer; counter: number }>(); - private fsWatchers: FileSystemWatcher[] = []; - 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<void>(); - disposableRegistry.push(this); - } - public get onDidCreate(): Event<void> { - return this.didCreate.event; - } - public dispose() { - this.clearTimers(); - } - @traceDecorators.verbose('Register Intepreter Watcher') - public async register(resource: Uri | undefined): Promise<void> { - if (this.fsWatchers.length > 0) { - return; - } - - 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; - Logger.verbose(`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('Intepreter Watcher change handler') - protected async createHandler(e: Uri) { - this.didCreate.fire(); - // 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(); - } - return this.timers.delete(pythonPath); - } - if (counter > (maxTimeToWaitForEnvCreation / timeToPollForEnvCreation)) { - // Send notification before we give up trying. - this.didCreate.fire(); - this.timers.delete(pythonPath); - return; - } - - const timer = setTimeout(() => this.notifyCreationWhenReady(pythonPath).ignoreErrors(), timeToPollForEnvCreation); - this.timers.set(pythonPath, { timer, counter }); - } - private clearTimers() { - this.timers.forEach(item => clearTimeout(item.timer)); - this.timers.clear(); - } - private async isValidExecutable(pythonPath: string): Promise<boolean> { - const execService = await this.pythonExecFactory.create({ pythonPath }); - const info = await execService.getInterpreterInformation().catch(() => undefined); - return info !== undefined; - } -} diff --git a/src/client/interpreter/locators/types.ts b/src/client/interpreter/locators/types.ts index d6cefcec2e6b..d67d8c1d7da0 100644 --- a/src/client/interpreter/locators/types.ts +++ b/src/client/interpreter/locators/types.ts @@ -3,7 +3,14 @@ 'use strict'; +import { Uri } from 'vscode'; + export const IPythonInPathCommandProvider = Symbol('IPythonInPathCommandProvider'); export interface IPythonInPathCommandProvider { getCommands(): { command: string; args?: string[] }[]; } +export const IPipEnvServiceHelper = Symbol('IPipEnvServiceHelper'); +export interface IPipEnvServiceHelper { + getPipEnvInfo(pythonPath: string): Promise<{ workspaceFolder: Uri; envName: string } | undefined>; + trackWorkspaceFolder(pythonPath: string, workspaceFolder: Uri): Promise<void>; +} diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 13cfc74065f0..f54f8e5368fe 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -1,116 +1,123 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +'use strict'; + +import { IExtensionActivationService, IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; +import { EnvironmentActivationService } from './activation/service'; +import { IEnvironmentActivationService } from './activation/types'; import { InterpreterAutoSelectionService } from './autoSelection/index'; -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 } from './autoSelection/types'; -import { InterpreterComparer } from './configuration/interpreterComparer'; -import { InterpreterSelector } from './configuration/interpreterSelector'; +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 { InterpreterSelector } from './configuration/interpreterSelector/interpreterSelector'; +import { RecommendedEnvironmentService } from './configuration/recommededEnvironmentService'; import { PythonPathUpdaterService } from './configuration/pythonPathUpdaterService'; import { PythonPathUpdaterServiceFactory } from './configuration/pythonPathUpdaterServiceFactory'; -import { IInterpreterComparer, IInterpreterSelector, IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } from './configuration/types'; import { - CONDA_ENV_FILE_SERVICE, - CONDA_ENV_SERVICE, - CURRENT_PATH_SERVICE, - GLOBAL_VIRTUAL_ENV_SERVICE, - ICondaService, - IInterpreterDisplay, - IInterpreterHelper, - IInterpreterLocatorHelper, - IInterpreterLocatorProgressService, - IInterpreterLocatorService, - IInterpreterService, - IInterpreterVersionService, - IInterpreterWatcher, - IInterpreterWatcherBuilder, - IKnownSearchPathsForInterpreters, - INTERPRETER_LOCATOR_SERVICE, - InterpreterLocatorProgressHandler, - IPipEnvService, - IShebangCodeLensProvider, - IVirtualEnvironmentsSearchPathProvider, - KNOWN_PATH_SERVICE, - PIPENV_SERVICE, - WINDOWS_REGISTRY_SERVICE, - WORKSPACE_VIRTUAL_ENV_SERVICE -} from './contracts'; + IInterpreterComparer, + IInterpreterQuickPick, + IInterpreterSelector, + IRecommendedEnvironmentService, + IPythonPathUpdaterServiceFactory, + IPythonPathUpdaterServiceManager, +} from './configuration/types'; +import { IActivatedEnvironmentLaunch, IInterpreterDisplay, IInterpreterHelper, IInterpreterService } from './contracts'; import { InterpreterDisplay } from './display'; -import { InterpreterLocatorProgressStatubarHandler } from './display/progressDisplay'; -import { ShebangCodeLensProvider } from './display/shebangCodeLensProvider'; +import { InterpreterLocatorProgressStatusBarHandler } from './display/progressDisplay'; import { InterpreterHelper } from './helpers'; +import { InterpreterPathCommand } from './interpreterPathCommand'; import { InterpreterService } from './interpreterService'; -import { InterpreterVersionService } from './interpreterVersion'; -import { InterpreterLocatorHelper } from './locators/helpers'; -import { PythonInterpreterLocatorService } from './locators/index'; -import { InterpreterLocatorProgressService } from './locators/progressService'; -import { CondaEnvFileService } from './locators/services/condaEnvFileService'; -import { CondaEnvService } from './locators/services/condaEnvService'; -import { CondaService } from './locators/services/condaService'; -import { CurrentPathService, PythonInPathCommandProvider } from './locators/services/currentPathService'; -import { GlobalVirtualEnvironmentsSearchPathProvider, GlobalVirtualEnvService } from './locators/services/globalVirtualEnvService'; -import { InterpreterWatcherBuilder } from './locators/services/interpreterWatcherBuilder'; -import { KnownPathsService, KnownSearchPathsForInterpreters } from './locators/services/KnownPathsService'; -import { PipEnvService } from './locators/services/pipEnvService'; -import { WindowsRegistryService } from './locators/services/windowsRegistryService'; -import { WorkspaceVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvService } from './locators/services/workspaceVirtualEnvService'; -import { WorkspaceVirtualEnvWatcherService } from './locators/services/workspaceVirtualEnvWatcherService'; -import { IPythonInPathCommandProvider } from './locators/types'; -import { VirtualEnvironmentManager } from './virtualEnvs/index'; -import { IVirtualEnvironmentManager } from './virtualEnvs/types'; - -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton<IKnownSearchPathsForInterpreters>(IKnownSearchPathsForInterpreters, KnownSearchPathsForInterpreters); - serviceManager.addSingleton<IVirtualEnvironmentsSearchPathProvider>(IVirtualEnvironmentsSearchPathProvider, GlobalVirtualEnvironmentsSearchPathProvider, 'global'); - serviceManager.addSingleton<IVirtualEnvironmentsSearchPathProvider>(IVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvironmentsSearchPathProvider, 'workspace'); +import { ActivatedEnvironmentLaunch } from './virtualEnvs/activatedEnvLaunch'; +import { CondaInheritEnvPrompt } from './virtualEnvs/condaInheritEnvPrompt'; +import { VirtualEnvironmentPrompt } from './virtualEnvs/virtualEnvPrompt'; - serviceManager.addSingleton<ICondaService>(ICondaService, CondaService); - serviceManager.addSingleton<IVirtualEnvironmentManager>(IVirtualEnvironmentManager, VirtualEnvironmentManager); - serviceManager.addSingleton<IPythonInPathCommandProvider>(IPythonInPathCommandProvider, PythonInPathCommandProvider); +/** + * Register all the new types inside this method. + * 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 + */ - serviceManager.add<IInterpreterWatcher>(IInterpreterWatcher, WorkspaceVirtualEnvWatcherService, WORKSPACE_VIRTUAL_ENV_SERVICE); - serviceManager.addSingleton<IInterpreterWatcherBuilder>(IInterpreterWatcherBuilder, InterpreterWatcherBuilder); +export function registerInterpreterTypes(serviceManager: IServiceManager): void { + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + InstallPythonCommand, + ); + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + InstallPythonViaTerminal, + ); + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + SetInterpreterCommand, + ); + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + ResetInterpreterCommand, + ); + serviceManager.addSingleton<IRecommendedEnvironmentService>( + IRecommendedEnvironmentService, + RecommendedEnvironmentService, + ); + serviceManager.addBinding(IRecommendedEnvironmentService, IExtensionActivationService); + serviceManager.addSingleton(IInterpreterQuickPick, SetInterpreterCommand); - serviceManager.addSingleton<IInterpreterVersionService>(IInterpreterVersionService, InterpreterVersionService); - serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, PythonInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); - serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, CondaEnvFileService, CONDA_ENV_FILE_SERVICE); - serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, CondaEnvService, CONDA_ENV_SERVICE); - serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, CurrentPathService, CURRENT_PATH_SERVICE); - serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, GlobalVirtualEnvService, GLOBAL_VIRTUAL_ENV_SERVICE); - serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, WorkspaceVirtualEnvService, WORKSPACE_VIRTUAL_ENV_SERVICE); - serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, PipEnvService, PIPENV_SERVICE); - serviceManager.addSingleton<IInterpreterLocatorService>(IPipEnvService, PipEnvService); + serviceManager.addSingleton<IExtensionActivationService>(IExtensionActivationService, VirtualEnvironmentPrompt); - serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, WindowsRegistryService, WINDOWS_REGISTRY_SERVICE); - serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, KnownPathsService, KNOWN_PATH_SERVICE); serviceManager.addSingleton<IInterpreterService>(IInterpreterService, InterpreterService); serviceManager.addSingleton<IInterpreterDisplay>(IInterpreterDisplay, InterpreterDisplay); + serviceManager.addBinding(IInterpreterDisplay, IExtensionSingleActivationService); - serviceManager.addSingleton<IPythonPathUpdaterServiceFactory>(IPythonPathUpdaterServiceFactory, PythonPathUpdaterServiceFactory); - serviceManager.addSingleton<IPythonPathUpdaterServiceManager>(IPythonPathUpdaterServiceManager, PythonPathUpdaterService); + serviceManager.addSingleton<IPythonPathUpdaterServiceFactory>( + IPythonPathUpdaterServiceFactory, + PythonPathUpdaterServiceFactory, + ); + serviceManager.addSingleton<IPythonPathUpdaterServiceManager>( + IPythonPathUpdaterServiceManager, + PythonPathUpdaterService, + ); serviceManager.addSingleton<IInterpreterSelector>(IInterpreterSelector, InterpreterSelector); - serviceManager.addSingleton<IShebangCodeLensProvider>(IShebangCodeLensProvider, ShebangCodeLensProvider); serviceManager.addSingleton<IInterpreterHelper>(IInterpreterHelper, InterpreterHelper); - serviceManager.addSingleton<IInterpreterLocatorHelper>(IInterpreterLocatorHelper, InterpreterLocatorHelper); - serviceManager.addSingleton<IInterpreterComparer>(IInterpreterComparer, InterpreterComparer); - serviceManager.addSingleton<InterpreterLocatorProgressHandler>(InterpreterLocatorProgressHandler, InterpreterLocatorProgressStatubarHandler); - serviceManager.addSingleton<IInterpreterLocatorProgressService>(IInterpreterLocatorProgressService, InterpreterLocatorProgressService); + serviceManager.addSingleton<IInterpreterComparer>(IInterpreterComparer, EnvironmentTypeComparer); + + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + InterpreterLocatorProgressStatusBarHandler, + ); + + serviceManager.addSingleton<IInterpreterAutoSelectionService>( + IInterpreterAutoSelectionService, + InterpreterAutoSelectionService, + ); + + serviceManager.addSingleton<IExtensionActivationService>(IExtensionActivationService, CondaInheritEnvPrompt); + serviceManager.addSingleton<IActivatedEnvironmentLaunch>(IActivatedEnvironmentLaunch, ActivatedEnvironmentLaunch); +} - serviceManager.addSingleton<IInterpreterAutoSelectionRule>(IInterpreterAutoSelectionRule, CurrentPathInterpretersAutoSelectionRule, AutoSelectionRule.currentPath); - serviceManager.addSingleton<IInterpreterAutoSelectionRule>(IInterpreterAutoSelectionRule, SystemWideInterpretersAutoSelectionRule, AutoSelectionRule.systemWide); - serviceManager.addSingleton<IInterpreterAutoSelectionRule>(IInterpreterAutoSelectionRule, WindowsRegistryInterpretersAutoSelectionRule, AutoSelectionRule.windowsRegistry); - serviceManager.addSingleton<IInterpreterAutoSelectionRule>(IInterpreterAutoSelectionRule, WorkspaceVirtualEnvInterpretersAutoSelectionRule, AutoSelectionRule.workspaceVirtualEnvs); - serviceManager.addSingleton<IInterpreterAutoSelectionRule>(IInterpreterAutoSelectionRule, CachedInterpretersAutoSelectionRule, AutoSelectionRule.cachedInterpreters); - serviceManager.addSingleton<IInterpreterAutoSelectionRule>(IInterpreterAutoSelectionRule, SettingsInterpretersAutoSelectionRule, AutoSelectionRule.settings); - serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>(IInterpreterAutoSeletionProxyService, InterpreterAutoSeletionProxyService); - serviceManager.addSingleton<IInterpreterAutoSelectionService>(IInterpreterAutoSelectionService, InterpreterAutoSelectionService); +export function registerTypes(serviceManager: IServiceManager): void { + registerInterpreterTypes(serviceManager); + serviceManager.addSingleton<IInterpreterAutoSelectionProxyService>( + IInterpreterAutoSelectionProxyService, + InterpreterAutoSelectionProxyService, + ); + serviceManager.addSingleton<IEnvironmentActivationService>( + EnvironmentActivationService, + EnvironmentActivationService, + ); + serviceManager.addSingleton<IEnvironmentActivationService>( + IEnvironmentActivationService, + EnvironmentActivationService, + ); + serviceManager.addSingleton<IExtensionSingleActivationService>( + 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<void> { + 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<string | undefined> { + if (this.wasSelected) { + return this.inMemorySelection; + } + return this._selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection); + } + + @cache(-1, true) + private async _selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection = false): Promise<string | undefined> { + 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<string | undefined> { + 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 new file mode 100644 index 000000000000..6b5295724449 --- /dev/null +++ b/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IExtensionActivationService } from '../../activation/types'; +import { IApplicationEnvironment, IApplicationShell, IWorkspaceService } from '../../common/application/types'; +import { IPlatformService } from '../../common/platform/types'; +import { IPersistentStateFactory } from '../../common/types'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { traceDecoratorError, traceError } from '../../logging'; +import { EnvironmentType } from '../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IInterpreterService } from '../contracts'; + +export const condaInheritEnvPromptKey = 'CONDA_INHERIT_ENV_PROMPT_KEY'; + +@injectable() +export class CondaInheritEnvPrompt implements IExtensionActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + constructor( + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IPlatformService) private readonly platformService: IPlatformService, + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, + public hasPromptBeenShownInCurrentSession: boolean = false, + ) {} + + public async activate(resource: Uri): Promise<void> { + this.initializeInBackground(resource).ignoreErrors(); + } + + @traceDecoratorError('Failed to intialize conda inherit env prompt') + public async initializeInBackground(resource: Uri): Promise<void> { + const show = await this.shouldShowPrompt(resource); + if (!show) { + return; + } + await this.promptAndUpdate(); + } + + @traceDecoratorError('Failed to display conda inherit env prompt') + public async promptAndUpdate() { + const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + condaInheritEnvPromptKey, + true, + ); + if (!notificationPromptEnabled.value) { + return; + } + 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, + }); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await this.workspaceService + .getConfiguration('terminal') + .update('integrated.inheritEnv', false, ConfigurationTarget.Global); + } else if (selection === prompts[1]) { + await notificationPromptEnabled.updateValue(false); + } + } + + @traceDecoratorError('Failed to check whether to display prompt for conda inherit env setting') + public async shouldShowPrompt(resource: Uri): Promise<boolean> { + 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.envType !== EnvironmentType.Conda) { + return false; + } + const setting = this.workspaceService + .getConfiguration('terminal', resource) + .inspect<boolean>('integrated.inheritEnv'); + if (!setting) { + traceError( + 'WorkspaceConfiguration.inspect returns `undefined` for setting `terminal.integrated.inheritEnv`', + ); + return false; + } + if ( + setting.globalValue !== undefined || + setting.workspaceValue !== undefined || + setting.workspaceFolderValue !== undefined + ) { + return false; + } + this.hasPromptBeenShownInCurrentSession = true; + return true; + } +} diff --git a/src/client/interpreter/virtualEnvs/index.ts b/src/client/interpreter/virtualEnvs/index.ts deleted file mode 100644 index 2c9224aeaeac..000000000000 --- a/src/client/interpreter/virtualEnvs/index.ts +++ /dev/null @@ -1,131 +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 { ITerminalActivationCommandProvider, TerminalShellType } from '../../common/terminal/types'; -import { ICurrentProcess, IPathUtils } from '../../common/types'; -import { getNamesAndValues } from '../../common/utils/enum'; -import { noop } from '../../common/utils/misc'; -import { IServiceContainer } from '../../ioc/types'; -import { InterpreterType, IPipEnvService } from '../contracts'; -import { IVirtualEnvironmentManager } from './types'; - -const PYENVFILES = ['pyvenv.cfg', path.join('..', 'pyvenv.cfg')]; - -@injectable() -export class VirtualEnvironmentManager implements IVirtualEnvironmentManager { - private processServiceFactory: IProcessServiceFactory; - private pipEnvService: IPipEnvService; - private fs: IFileSystem; - private pyEnvRoot?: string; - private workspaceService: IWorkspaceService; - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { - this.processServiceFactory = serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory); - this.fs = serviceContainer.get<IFileSystem>(IFileSystem); - this.pipEnvService = serviceContainer.get<IPipEnvService>(IPipEnvService); - this.workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService); - } - public async getEnvironmentName(pythonPath: string, resource?: Uri): Promise<string> { - const defaultWorkspaceUri = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders![0].uri : undefined; - const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; - const workspaceUri = workspaceFolder ? workspaceFolder.uri : defaultWorkspaceUri; - const grandParentDirName = path.basename(path.dirname(path.dirname(pythonPath))); - if (workspaceUri && await this.pipEnvService.isRelatedPipEnvironment(workspaceUri.fsPath, pythonPath)) { - // In pipenv, return the folder name of the workspace. - return path.basename(workspaceUri.fsPath); - } - - return grandParentDirName; - } - public async getEnvironmentType(pythonPath: string, resource?: Uri): Promise<InterpreterType> { - if (await this.isVenvEnvironment(pythonPath)) { - return InterpreterType.Venv; - } - - if (await this.isPyEnvEnvironment(pythonPath, resource)) { - return InterpreterType.Pyenv; - } - - if (await this.isPipEnvironment(pythonPath, resource)) { - return InterpreterType.PipEnv; - } - - if (await this.isVirtualEnvironment(pythonPath)) { - return InterpreterType.VirtualEnv; - } - - // Lets not try to determine whether this is a conda environment or not. - return InterpreterType.Unknown; - } - public async isVenvEnvironment(pythonPath: string) { - const dir = path.dirname(pythonPath); - const pyEnvCfgFiles = PYENVFILES.map(file => path.join(dir, file)); - for (const file of pyEnvCfgFiles) { - if (await this.fs.fileExists(file)) { - return true; - } - } - return false; - } - public async isPyEnvEnvironment(pythonPath: string, resource?: Uri) { - const pyEnvRoot = await this.getPyEnvRoot(resource); - return pyEnvRoot && pythonPath.startsWith(pyEnvRoot); - } - public async isPipEnvironment(pythonPath: string, resource?: Uri) { - const defaultWorkspaceUri = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders![0].uri : undefined; - const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; - const workspaceUri = workspaceFolder ? workspaceFolder.uri : defaultWorkspaceUri; - if (workspaceUri && await this.pipEnvService.isRelatedPipEnvironment(workspaceUri.fsPath, pythonPath)) { - return true; - } - return false; - } - public async getPyEnvRoot(resource?: Uri): Promise<string | undefined> { - if (this.pyEnvRoot) { - return this.pyEnvRoot; - } - - const currentProccess = this.serviceContainer.get<ICurrentProcess>(ICurrentProcess); - const pyenvRoot = currentProccess.env.PYENV_ROOT; - if (pyenvRoot) { - return this.pyEnvRoot = pyenvRoot; - } - - try { - const processService = await this.processServiceFactory.create(resource); - const output = await processService.exec('pyenv', ['root']); - if (output.stdout.trim().length > 0) { - return this.pyEnvRoot = output.stdout.trim(); - } - } catch { - noop(); - } - const pathUtils = this.serviceContainer.get<IPathUtils>(IPathUtils); - return this.pyEnvRoot = path.join(pathUtils.home, '.pyenv'); - } - public async isVirtualEnvironment(pythonPath: string) { - const provider = this.getTerminalActivationProviderForVirtualEnvs(); - const shells = getNamesAndValues<TerminalShellType>(TerminalShellType) - .filter(shell => provider.isShellSupported(shell.value)) - .map(shell => shell.value); - - for (const shell of shells) { - const cmds = await provider.getActivationCommandsForInterpreter!(pythonPath, shell); - if (cmds && cmds.length > 0) { - return true; - } - } - - return false; - } - private getTerminalActivationProviderForVirtualEnvs(): ITerminalActivationCommandProvider { - const isWindows = this.serviceContainer.get<IPlatformService>(IPlatformService).isWindows; - const serviceName = isWindows ? 'commandPromptAndPowerShell' : 'bashCShellFish'; - return this.serviceContainer.get<ITerminalActivationCommandProvider>(ITerminalActivationCommandProvider, serviceName); - } -} diff --git a/src/client/interpreter/virtualEnvs/types.ts b/src/client/interpreter/virtualEnvs/types.ts deleted file mode 100644 index 98865f0edc85..000000000000 --- a/src/client/interpreter/virtualEnvs/types.ts +++ /dev/null @@ -1,12 +0,0 @@ - -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { InterpreterType } from '../contracts'; -export const IVirtualEnvironmentManager = Symbol('VirtualEnvironmentManager'); -export interface IVirtualEnvironmentManager { - getEnvironmentName(pythonPath: string, resource?: Uri): Promise<string>; - getEnvironmentType(pythonPath: string, resource?: Uri): Promise<InterpreterType>; - getPyEnvRoot(resource?: Uri): Promise<string | undefined>; -} diff --git a/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts b/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts new file mode 100644 index 000000000000..7ed18c0e8b2a --- /dev/null +++ b/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { ConfigurationTarget, Disposable, Uri } from 'vscode'; +import { IExtensionActivationService } from '../../activation/types'; +import { IApplicationShell } from '../../common/application/types'; +import { IDisposableRegistry, IPersistentStateFactory } from '../../common/types'; +import { Common, Interpreters } from '../../common/utils/localize'; +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 { 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(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, + @inject(IPythonPathUpdaterServiceManager) + private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager, + @inject(IDisposableRegistry) private readonly disposableRegistry: Disposable[], + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + ) {} + + public async activate(resource: Uri): Promise<void> { + const disposable = this.pyenvs.onDidCreate(resource, () => this.handleNewEnvironment(resource)); + this.disposableRegistry.push(disposable); + } + + @traceDecoratorError('Error in event handler for detection of new environment') + protected async handleNewEnvironment(resource: Uri): Promise<void> { + 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: PythonEnvironment, resource: Uri): Promise<void> { + const notificationPromptEnabled = this.persistentStateFactory.createWorkspacePersistentState( + doNotDisplayPromptStateKey, + true, + ); + if (!notificationPromptEnabled.value) { + return; + } + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; + const telemetrySelections: ['Yes', 'No', 'Ignore'] = ['Yes', 'No', 'Ignore']; + const selection = await this.appShell.showInformationMessage(Interpreters.environmentPromptMessage, ...prompts); + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, + }); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await this.pythonPathUpdaterService.updatePythonPath( + interpreter.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + resource, + ); + } else if (selection === prompts[2]) { + await notificationPromptEnabled.updateValue(false); + } + } +} diff --git a/src/client/ioc/container.ts b/src/client/ioc/container.ts index 22c2d30d1d6c..0f1302061a67 100644 --- a/src/client/ioc/container.ts +++ b/src/client/ioc/container.ts @@ -3,25 +3,48 @@ import { EventEmitter } from 'events'; import { Container, decorate, injectable, interfaces } from 'inversify'; +import { traceWarn } from '../logging'; import { Abstract, IServiceContainer, Newable } from './types'; // This needs to be done once, hence placed in a common location. // Used by UnitTestSockerServer and also the extension unit tests. // Place within try..catch, as this can only be done once (it's -// possible another extesion would perform this before our extension). +// possible another extension would perform this before our extension). try { decorate(injectable(), EventEmitter); } catch (ex) { - console.warn('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) { } + constructor(private container: Container) {} + public get<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, name?: string | number | symbol): T { return name ? this.container.getNamed<T>(serviceIdentifier, name) : this.container.get<T>(serviceIdentifier); } - public getAll<T>(serviceIdentifier: string | symbol | Newable<T> | Abstract<T>, name?: string | number | symbol | undefined): T[] { - return name ? this.container.getAllNamed<T>(serviceIdentifier, name) : this.container.getAll<T>(serviceIdentifier); + + public getAll<T>( + serviceIdentifier: string | symbol | Newable<T> | Abstract<T>, + name?: string | number | symbol | undefined, + ): T[] { + return name + ? this.container.getAllNamed<T>(serviceIdentifier, name) + : this.container.getAll<T>(serviceIdentifier); + } + + public tryGet<T>( + serviceIdentifier: interfaces.ServiceIdentifier<T>, + name?: string | number | symbol | undefined, + ): T | undefined { + try { + return name + ? this.container.getNamed<T>(serviceIdentifier, name) + : this.container.get<T>(serviceIdentifier); + } 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 d316409a385b..a575b25e8c3f 100644 --- a/src/client/ioc/serviceManager.ts +++ b/src/client/ioc/serviceManager.ts @@ -8,43 +8,100 @@ type identifier<T> = string | symbol | Newable<T> | Abstract<T>; @injectable() export class ServiceManager implements IServiceManager { - constructor(private container: Container) { } - // tslint:disable-next-line:no-any - public add<T>(serviceIdentifier: identifier<T>, constructor: new (...args: any[]) => T, name?: string | number | symbol | undefined): void { + constructor(private container: Container) {} + + public add<T>( + serviceIdentifier: identifier<T>, + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor: new (...args: any[]) => T, + name?: string | number | symbol | undefined, + bindings?: symbol[], + ): void { if (name) { this.container.bind<T>(serviceIdentifier).to(constructor).whenTargetNamed(name); } else { this.container.bind<T>(serviceIdentifier).to(constructor); } + + if (bindings) { + bindings.forEach((binding) => { + this.addBinding(serviceIdentifier, binding); + }); + } } - // tslint:disable-next-line:no-any - public addFactory<T>(factoryIdentifier: interfaces.ServiceIdentifier<interfaces.Factory<T>>, factoryMethod: interfaces.FactoryCreator<T>): void { + + public addFactory<T>( + factoryIdentifier: interfaces.ServiceIdentifier<interfaces.Factory<T>>, + factoryMethod: interfaces.FactoryCreator<T>, + ): void { this.container.bind<interfaces.Factory<T>>(factoryIdentifier).toFactory<T>(factoryMethod); } - // tslint:disable-next-line:no-any - public addSingleton<T>(serviceIdentifier: identifier<T>, constructor: new (...args: any[]) => T, name?: string | number | symbol | undefined): void { + + public addBinding<T1, T2>(from: identifier<T1>, to: identifier<T2>): void { + this.container.bind(to).toService(from); + } + + public addSingleton<T>( + serviceIdentifier: identifier<T>, + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor: new (...args: any[]) => T, + name?: string | number | symbol | undefined, + bindings?: symbol[], + ): void { if (name) { this.container.bind<T>(serviceIdentifier).to(constructor).inSingletonScope().whenTargetNamed(name); } else { this.container.bind<T>(serviceIdentifier).to(constructor).inSingletonScope(); } + + if (bindings) { + bindings.forEach((binding) => { + this.addBinding(serviceIdentifier, binding); + }); + } } - // tslint:disable-next-line:no-any - public addSingletonInstance<T>(serviceIdentifier: identifier<T>, instance: T, name?: string | number | symbol | undefined): void { + + public addSingletonInstance<T>( + serviceIdentifier: identifier<T>, + instance: T, + name?: string | number | symbol | undefined, + ): void { if (name) { this.container.bind<T>(serviceIdentifier).toConstantValue(instance).whenTargetNamed(name); } else { this.container.bind<T>(serviceIdentifier).toConstantValue(instance); } } + public get<T>(serviceIdentifier: identifier<T>, name?: string | number | symbol | undefined): T { return name ? this.container.getNamed<T>(serviceIdentifier, name) : this.container.get<T>(serviceIdentifier); } + + public tryGet<T>(serviceIdentifier: identifier<T>, name?: string | number | symbol | undefined): T | undefined { + try { + return name + ? this.container.getNamed<T>(serviceIdentifier, name) + : this.container.get<T>(serviceIdentifier); + } catch { + // This might happen after the container has been destroyed + } + + return undefined; + } + public getAll<T>(serviceIdentifier: identifier<T>, name?: string | number | symbol | undefined): T[] { - return name ? this.container.getAllNamed<T>(serviceIdentifier, name) : this.container.getAll<T>(serviceIdentifier); + return name + ? this.container.getAllNamed<T>(serviceIdentifier, name) + : this.container.getAll<T>(serviceIdentifier); } - public rebind<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, constructor: ClassType<T>, name?: string | number | symbol): void { + public rebind<T>( + serviceIdentifier: interfaces.ServiceIdentifier<T>, + constructor: ClassType<T>, + name?: string | number | symbol, + ): void { if (name) { this.container.rebind<T>(serviceIdentifier).to(constructor).whenTargetNamed(name); } else { @@ -52,7 +109,23 @@ export class ServiceManager implements IServiceManager { } } - public rebindInstance<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, instance: T, name?: string | number | symbol): void { + public rebindSingleton<T>( + serviceIdentifier: interfaces.ServiceIdentifier<T>, + constructor: ClassType<T>, + name?: string | number | symbol, + ): void { + if (name) { + this.container.rebind<T>(serviceIdentifier).to(constructor).inSingletonScope().whenTargetNamed(name); + } else { + this.container.rebind<T>(serviceIdentifier).to(constructor).inSingletonScope(); + } + } + + public rebindInstance<T>( + serviceIdentifier: interfaces.ServiceIdentifier<T>, + instance: T, + name?: string | number | symbol, + ): void { if (name) { this.container.rebind<T>(serviceIdentifier).toConstantValue(instance).whenTargetNamed(name); } else { @@ -60,4 +133,8 @@ export class ServiceManager implements IServiceManager { } } + public dispose(): void { + this.container.unbindAll(); + this.container.unload(); + } } diff --git a/src/client/ioc/types.ts b/src/client/ioc/types.ts index 9520cfa4c6ea..0a18e44824ed 100644 --- a/src/client/ioc/types.ts +++ b/src/client/ioc/types.ts @@ -2,36 +2,70 @@ // Licensed under the MIT License. import { interfaces } from 'inversify'; +import { IDisposable } from '../common/types'; -// tslint:disable-next-line:interface-name export interface Newable<T> { - // tslint:disable-next-line:no-any - new(...args: any[]): T; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new (...args: any[]): T; } -// tslint:disable-next-line:interface-name + export interface Abstract<T> { prototype: T; } + export type ClassType<T> = { - // tslint:disable-next-line:no-any - new(...args: any[]): T; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new (...args: any[]): T; }; export const IServiceManager = Symbol('IServiceManager'); -export interface IServiceManager { - add<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, constructor: ClassType<T>, name?: string | number | symbol): void; - addSingleton<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, constructor: ClassType<T>, name?: string | number | symbol): void; - addSingletonInstance<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, instance: T, name?: string | number | symbol): void; - addFactory<T>(factoryIdentifier: interfaces.ServiceIdentifier<interfaces.Factory<T>>, factoryMethod: interfaces.FactoryCreator<T>): void; +export interface IServiceManager extends IDisposable { + add<T>( + serviceIdentifier: interfaces.ServiceIdentifier<T>, + constructor: ClassType<T>, + name?: string | number | symbol | undefined, + bindings?: symbol[], + ): void; + addSingleton<T>( + serviceIdentifier: interfaces.ServiceIdentifier<T>, + constructor: ClassType<T>, + name?: string | number | symbol, + bindings?: symbol[], + ): void; + addSingletonInstance<T>( + serviceIdentifier: interfaces.ServiceIdentifier<T>, + instance: T, + name?: string | number | symbol, + ): void; + addFactory<T>( + factoryIdentifier: interfaces.ServiceIdentifier<interfaces.Factory<T>>, + factoryMethod: interfaces.FactoryCreator<T>, + ): void; + addBinding<T1, T2>(from: interfaces.ServiceIdentifier<T1>, to: interfaces.ServiceIdentifier<T2>): void; get<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, name?: string | number | symbol): T; + tryGet<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, name?: string | number | symbol): T | undefined; getAll<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, name?: string | number | symbol): T[]; - rebind<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, constructor: ClassType<T>, name?: string | number | symbol): void; - rebindInstance<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, instance: T, name?: string | number | symbol): void; + rebind<T>( + serviceIdentifier: interfaces.ServiceIdentifier<T>, + constructor: ClassType<T>, + name?: string | number | symbol, + ): void; + rebindSingleton<T>( + serviceIdentifier: interfaces.ServiceIdentifier<T>, + constructor: ClassType<T>, + name?: string | number | symbol, + ): void; + rebindInstance<T>( + serviceIdentifier: interfaces.ServiceIdentifier<T>, + instance: T, + name?: string | number | symbol, + ): void; } export const IServiceContainer = Symbol('IServiceContainer'); export interface IServiceContainer { get<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, name?: string | number | symbol): T; getAll<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, name?: string | number | symbol): T[]; + tryGet<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, name?: string | number | symbol): T | undefined; } 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<NodeJS.ProcessEnv | undefined>; + getKnownSuggestions(resource: Resource): IInterpreterQuickPickItem[]; + /** + * @deprecated Use `getKnownSuggestions` and `suggestionToQuickPickItem` instead. + */ + getSuggestions(resource: Resource): Promise<IInterpreterQuickPickItem[]>; + /** + * Returns path to where `debugpy` is. In python extension this is `/python_files/lib/python`. + */ + getDebuggerPath(): Promise<string>; + /** + * 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<SemVer | undefined>; + /** + * Returns the conda executable. + */ + getCondaFile(): Promise<string | undefined>; + + /** + * 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<string | undefined>): 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<JupyterExtensionApi> | undefined; + + private pylanceExtension: Extension<PylanceApi> | 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<IInterpreterQuickPickItem[]> => + this.interpreterSelector.getAllSuggestions(resource), + getKnownSuggestions: (resource: Resource): IInterpreterQuickPickItem[] => + this.interpreterSelector.getSuggestions(resource), + getDebuggerPath: async () => dirname(await getDebugpyPath()), + getInterpreterPathSelectedForJupyterServer: () => + this.globalState.get<string | undefined>('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<string | undefined>) => + this.registerJupyterPythonPathFunction(func), + getRecommededEnvironment: async (uri) => { + if (!this.environmentApi) { + return undefined; + } + return this.preferredEnvironmentService.getRecommededEnvironment(uri); + }, + }); + return undefined; + } + + public async integrateWithJupyterExtension(): Promise<void> { + const api = await this.getExtensionApi(); + if (api) { + this.registerApi(api); + } + } + + private async getExtensionApi(): Promise<JupyterExtensionApi | undefined> { + if (!this.pylanceExtension) { + const pylanceExtension = this.extensions.getExtension<PylanceApi>(PYLANCE_EXTENSION_ID); + + if (pylanceExtension && !pylanceExtension.isActive) { + await pylanceExtension.activate(); + } + + this.pylanceExtension = pylanceExtension; + } + + if (!this.jupyterExtension) { + const jupyterExtension = this.extensions.getExtension<JupyterExtensionApi>(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<string | undefined>) { + 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<Uri>; + /** + * 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<Uri>()); + + 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<JupyterPythonEnvironmentApi>(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/provider.ts b/src/client/jupyter/provider.ts deleted file mode 100644 index 3b4b1fbafaf9..000000000000 --- a/src/client/jupyter/provider.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Range, window, TextDocument, Position } from 'vscode'; - -export class JupyterProvider { - /** - * Returns a Regular Expression used to determine whether a line is a Cell delimiter or not - * - * @type {RegExp} - * @memberOf LanguageProvider - */ - get cellIdentifier(): RegExp { - return /^(# %%|#%%|# \<codecell\>|# In\[\d*?\]|# In\[ \])(.*)/i; - } - - /** - * Returns the selected code - * If not implemented, then the currently active line or selected code is taken. - * Can be implemented to ensure valid blocks of code are selected. - * E.g if user selects only the If statement, code can be impelemented to ensure all code within the if statement (block) is returned - * @param {string} selectedCode The selected code as identified by this extension. - * @param {Range} [currentCell] Range of the currently active cell - * @returns {Promise<string>} The code selected. If nothing is to be done, return the parameter value. - * - * @memberOf LanguageProvider - */ - getSelectedCode(selectedCode: string, currentCell?: Range): Promise<string> { - if (!JupyterProvider.isCodeBlock(selectedCode)) { - return Promise.resolve(selectedCode); - } - - // ok we're in a block, look for the end of the block untill the last line in the cell (if there are any cells) - return new Promise<string>((resolve, reject) => { - const activeEditor = window.activeTextEditor; - const endLineNumber = currentCell ? currentCell.end.line : activeEditor.document.lineCount - 1; - const startIndent = selectedCode.indexOf(selectedCode.trim()); - const nextStartLine = activeEditor.selection.start.line + 1; - - for (let lineNumber = nextStartLine; lineNumber <= endLineNumber; lineNumber++) { - const line = activeEditor.document.lineAt(lineNumber); - const nextLine = line.text; - const nextLineIndent = nextLine.indexOf(nextLine.trim()); - if (nextLine.trim().indexOf('#') === 0) { - continue; - } - if (nextLineIndent === startIndent) { - // Return code untill previous line - const endRange = activeEditor.document.lineAt(lineNumber - 1).range.end; - resolve(activeEditor.document.getText(new Range(activeEditor.selection.start, endRange))); - } - } - - resolve(activeEditor.document.getText(currentCell)); - }); - } - - /** - * Gets the first line (position) of executable code within a range - * - * @param {TextDocument} document - * @param {number} startLine - * @param {number} endLine - * @returns {Promise<Position>} - * - * @memberOf LanguageProvider - */ - getFirstLineOfExecutableCode(document: TextDocument, range: Range): Promise<Position> { - for (let lineNumber = range.start.line; lineNumber < range.end.line; lineNumber++) { - let line = document.lineAt(lineNumber); - if (line.isEmptyOrWhitespace) { - continue; - } - const lineText = line.text; - const trimmedLine = lineText.trim(); - if (trimmedLine.startsWith('#')) { - continue; - } - // Yay we have a line - // Remember, we need to set the cursor to a character other than white space - // Highlighting doesn't kick in for comments or white space - return Promise.resolve(new Position(lineNumber, lineText.indexOf(trimmedLine))); - } - - // give up - return Promise.resolve(new Position(range.start.line, 0)); - } - - private static isCodeBlock(code: string): boolean { - return code.trim().endsWith(':') && code.indexOf('#') === -1; - } - -} \ No newline at end of file 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<void> { + this.disposables.push(this.commandManager.registerCommand(Commands.InstallJupyter, () => this._showPrompt())); + } + + public async _showPrompt(): Promise<void> { + 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<JupyterServerUriHandle | 'back' | undefined>; + getServerUri(handle: JupyterServerUriHandle): Promise<IJupyterServerUri>; +} + +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>; +} + +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 5a4da26a7b6d..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 6f92e1e769de..000000000000 --- a/src/client/language/iterableTextRange.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { ITextRange, ITextRangeCollection } from './types'; - -export class IterableTextRange<T extends ITextRange> implements Iterable<T>{ - constructor(private textRangeCollection: ITextRangeCollection<T>) { - } - public [Symbol.iterator](): Iterator<T> { - let index = -1; - - return { - next: (): IteratorResult<T> => { - if (index < this.textRangeCollection.count - 1) { - return { - done: false, - value: this.textRangeCollection.getItemAt(index += 1) - }; - } else { - return { - done: true, - // tslint:disable-next-line:no-any - value: undefined as any - }; - } - } - }; - } -} diff --git a/src/client/language/languageConfiguration.ts b/src/client/language/languageConfiguration.ts new file mode 100644 index 000000000000..0fbcd29c645a --- /dev/null +++ b/src/client/language/languageConfiguration.ts @@ -0,0 +1,120 @@ +// 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'; + +export function getLanguageConfiguration(): LanguageConfiguration { + return { + onEnterRules: [ + // multi-line separator + { + beforeText: verboseRegExp(` + ^ + (?! \\s+ \\\\ ) + [^#\n]+ + \\\\ + $ + `), + action: { + indentAction: IndentAction.Indent, + }, + }, + // continue comments + { + beforeText: /^\s*#.*/, + afterText: /.+$/, + action: { + indentAction: IndentAction.None, + appendText: '# ', + }, + }, + // indent on enter (block-beginning statements) + { + /** + * This does not handle all cases. However, it does handle nearly all usage. + * Here's what it does not cover: + * - the statement is split over multiple lines (and hence the ":" is on a different line) + * - the code block is inlined (after the ":") + * - there are multiple statements on the line (separated by semicolons) + * Also note that `lambda` is purposefully excluded. + */ + beforeText: verboseRegExp(` + ^ + \\s* + (?: + (?: + (?: + class | + def | + async \\s+ def | + except | + for | + async \\s+ for | + if | + elif | + while | + with | + async \\s+ with | + match | + case + ) + \\b .* + ) | + else | + try | + finally + ) + \\s* + [:] + \\s* + (?: [#] .* )? + $ + `), + action: { + 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 + ) + ) | + (?: + \\s+ + (?: + raise | + break | + continue + ) + ) + ) + \\s* + (?: [#] .* )? + $ + `), + action: { + indentAction: IndentAction.Outdent, + }, + }, + // Note that we do not currently have an auto-dedent + // solution for "elif", "else", "except", and "finally". + // We had one but had to remove it (see issue #6886). + ], + }; +} diff --git a/src/client/language/textBuilder.ts b/src/client/language/textBuilder.ts 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 d5eda4783e2c..000000000000 --- a/src/client/language/textIterator.ts +++ /dev/null @@ -1,55 +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<T extends ITextRange> implements ITextRangeCollection<T> { - 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 50269ab27638..000000000000 --- a/src/client/language/tokenizer.ts +++ /dev/null @@ -1,500 +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, isWhiteSpace } 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<IToken>; - public tokenize(text: string, start: number, length: number, mode: TokenizerMode): ITextRangeCollection<IToken>; - - public tokenize(text: string, start?: number, length?: number, mode?: TokenizerMode): ITextRangeCollection<IToken> { - 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 76c133a9061c..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<T> 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); - 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<IToken>; - tokenize(text: string, start: number, length: number, mode: TokenizerMode): ITextRangeCollection<IToken>; -} 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<void> { + await this.serverManager.start(resource, interpreter); + this.serverManager.connect(); + } + + async stopLanguageServer(): Promise<void> { + 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<void> { + // 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<void> { + return Promise.resolve(); + } + + stopLanguageServer(): Promise<void> { + return Promise.resolve(); + } + + canStartLanguageServer(): boolean { + return true; + } + + languageServerNotAvailable(): Promise<void> { + // 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<void> { + await this.serverManager.start(resource, interpreter); + this.serverManager.connect(); + } + + async stopLanguageServer(): Promise<void> { + this.serverManager.disconnect(); + await this.serverProxy.stop(); + } + + canStartLanguageServer(): boolean { + const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + return !!extension; + } + + async languageServerNotAvailable(): Promise<void> { + 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<void>; + restartLanguageServers(): Promise<void>; + get(resource: Resource, interpreter?: PythonEnvironment): Promise<ILanguageServerExtensionManager>; +} + +/** + * `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<void>; + stopLanguageServer(): Promise<void>; + canStartLanguageServer(interpreter: PythonEnvironment | undefined): boolean; + languageServerNotAvailable(): Promise<void>; + 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<string, PythonEnvironment | undefined>; + + // 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<string, ILanguageServerExtensionManager | undefined>; + + 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<void> { + this.register(); + await this.startLanguageServer(this.languageServerType, resource, startupStopWatch); + } + + // ILanguageServerWatcher + public async startLanguageServer( + languageServerType: LanguageServerType, + resource?: Resource, + startupStopWatch?: StopWatch, + ): Promise<void> { + 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<ILanguageServerExtensionManager> { + 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<void> { + 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<ILanguageServerExtensionManager> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + const languageServerType = this.configurationService.getSettings().languageServer; + + if (languageServerType !== this.languageServerType) { + await this.refreshLanguageServer(); + } + } + + // Watch for workspace folder changes. + private async onDidChangeWorkspaceFolders(event: WorkspaceFoldersChangeEvent): Promise<void> { + // 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 5e18b2396e51..000000000000 --- a/src/client/languageServices/jediProxyFactory.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Disposable, Uri, workspace } from 'vscode'; -import { IServiceContainer } from '../ioc/types'; -import { ICommandResult, JediProxy, JediProxyHandler } from '../providers/jediProxy'; - -export class JediFactory implements Disposable { - private disposables: Disposable[]; - private jediProxyHandlers: Map<string, JediProxyHandler<ICommandResult>>; - - constructor(private extensionRootPath: string, private serviceContainer: IServiceContainer) { - this.disposables = []; - this.jediProxyHandlers = new Map<string, JediProxyHandler<ICommandResult>>(); - } - - public dispose() { - this.disposables.forEach(disposable => disposable.dispose()); - this.disposables = []; - } - public getJediProxyHandler<T extends ICommandResult>(resource?: Uri): JediProxyHandler<T> { - const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; - let workspacePath = workspaceFolder ? workspaceFolder.uri.fsPath : undefined; - if (!workspacePath) { - if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { - workspacePath = workspace.workspaceFolders[0].uri.fsPath; - } else { - workspacePath = __dirname; - } - } - - if (!this.jediProxyHandlers.has(workspacePath)) { - const jediProxy = new JediProxy(this.extensionRootPath, workspacePath, this.serviceContainer); - const jediProxyHandler = new JediProxyHandler(jediProxy); - this.disposables.push(jediProxy, jediProxyHandler); - this.jediProxyHandlers.set(workspacePath, jediProxyHandler); - } - // tslint:disable-next-line:no-non-null-assertion - return this.jediProxyHandlers.get(workspacePath)! as JediProxyHandler<T>; - } -} diff --git a/src/client/languageServices/languageServerSurveyBanner.ts b/src/client/languageServices/languageServerSurveyBanner.ts deleted file mode 100644 index d2156a71e21c..000000000000 --- a/src/client/languageServices/languageServerSurveyBanner.ts +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { FolderVersionPair, ILanguageServerFolderService } from '../activation/types'; -import { IApplicationShell } from '../common/application/types'; -import '../common/extensions'; -import { - IBrowserService, IPersistentStateFactory, - IPythonExtensionBanner -} from '../common/types'; -import * as localize from '../common/utils/localize'; -import { getRandomBetween } from '../common/utils/random'; - -// persistent state names, exported to make use of in testing -export enum LSSurveyStateKeys { - ShowBanner = 'ShowLSSurveyBanner', - ShowAttemptCounter = 'LSSurveyShowAttempt', - ShowAfterCompletionCount = 'LSSurveyShowCount' -} - -enum LSSurveyLabelIndex { - Yes, - No -} - -/* -This class represents a popup that will ask our users for some feedback after -a specific event occurs N times. -*/ -@injectable() -export class LanguageServerSurveyBanner implements IPythonExtensionBanner { - private disabledInCurrentSession: boolean = false; - private minCompletionsBeforeShow: number; - private maxCompletionsBeforeShow: number; - private isInitialized: boolean = false; - private bannerMessage: string = localize.LanguageService.bannerMessage(); - private bannerLabels: string[] = [localize.LanguageService.bannerLabelYes(), localize.LanguageService.bannerLabelNo()]; - - constructor( - @inject(IApplicationShell) private appShell: IApplicationShell, - @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, - @inject(IBrowserService) private browserService: IBrowserService, - @inject(ILanguageServerFolderService) private lsService: ILanguageServerFolderService, - showAfterMinimumEventsCount: number = 100, - showBeforeMaximumEventsCount: number = 500) { - this.minCompletionsBeforeShow = showAfterMinimumEventsCount; - this.maxCompletionsBeforeShow = showBeforeMaximumEventsCount; - this.initialize(); - } - - public initialize(): void { - if (this.isInitialized) { - return; - } - this.isInitialized = true; - - if (this.minCompletionsBeforeShow >= this.maxCompletionsBeforeShow) { - this.disable().ignoreErrors(); - } - } - - public get optionLabels(): string[] { - return this.bannerLabels; - } - - public get shownCount(): Promise<number> { - return this.getPythonLSLaunchCounter(); - } - - public get enabled(): boolean { - return this.persistentState.createGlobalPersistentState<boolean>(LSSurveyStateKeys.ShowBanner, true).value; - } - - public async showBanner(): Promise<void> { - if (!this.enabled || this.disabledInCurrentSession) { - return; - } - - const launchCounter: number = await this.incrementPythonLanguageServiceLaunchCounter(); - const show = await this.shouldShowBanner(launchCounter); - if (!show) { - return; - } - - const response = await this.appShell.showInformationMessage(this.bannerMessage, ...this.bannerLabels); - switch (response) { - case this.bannerLabels[LSSurveyLabelIndex.Yes]: - { - await this.launchSurvey(); - await this.disable(); - break; - } - case this.bannerLabels[LSSurveyLabelIndex.No]: { - await this.disable(); - break; - } - default: { - // Disable for the current session. - this.disabledInCurrentSession = true; - } - } - } - - public async shouldShowBanner(launchCounter?: number): Promise<boolean> { - if (!this.enabled || this.disabledInCurrentSession) { - return false; - } - - if (!launchCounter) { - launchCounter = await this.getPythonLSLaunchCounter(); - } - const threshold: number = await this.getPythonLSLaunchThresholdCounter(); - - return launchCounter >= threshold; - } - - public async disable(): Promise<void> { - await this.persistentState.createGlobalPersistentState<boolean>(LSSurveyStateKeys.ShowBanner, false).updateValue(false); - } - - public async launchSurvey(): Promise<void> { - const launchCounter = await this.getPythonLSLaunchCounter(); - let lsVersion: string = await this.getPythonLSVersion(); - lsVersion = encodeURIComponent(lsVersion); - this.browserService.launch(`https://www.research.net/r/LJZV9BZ?n=${launchCounter}&v=${lsVersion}`); - } - - private async incrementPythonLanguageServiceLaunchCounter(): Promise<number> { - const state = this.persistentState.createGlobalPersistentState<number>(LSSurveyStateKeys.ShowAttemptCounter, 0); - await state.updateValue(state.value + 1); - return state.value; - } - - private async getPythonLSVersion(fallback: string = 'unknown'): Promise<string> { - const langServiceLatestFolder: FolderVersionPair | undefined = await this.lsService.getCurrentLanguageServerDirectory(); - return langServiceLatestFolder ? langServiceLatestFolder.version.raw : fallback; - } - - private async getPythonLSLaunchCounter(): Promise<number> { - const state = this.persistentState.createGlobalPersistentState<number>(LSSurveyStateKeys.ShowAttemptCounter, 0); - return state.value; - } - - private async getPythonLSLaunchThresholdCounter(): Promise<number> { - const state = this.persistentState.createGlobalPersistentState<number | undefined>(LSSurveyStateKeys.ShowAfterCompletionCount, undefined); - if (state.value === undefined) { - await state.updateValue(getRandomBetween(this.minCompletionsBeforeShow, this.maxCompletionsBeforeShow)); - } - return state.value!; - } -} diff --git a/src/client/languageServices/proposeLanguageServerBanner.ts b/src/client/languageServices/proposeLanguageServerBanner.ts deleted file mode 100644 index c58c4697a114..000000000000 --- a/src/client/languageServices/proposeLanguageServerBanner.ts +++ /dev/null @@ -1,124 +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 { getRandomBetween } from '../common/utils/random'; - -// persistent state names, exported to make use of in testing -export enum ProposeLSStateKeys { - ShowBanner = 'ProposeLSBanner' -} - -enum ProposeLSLabelIndex { - Yes, - No, - Later -} - -/* -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 ProposeLanguageServerBanner implements IPythonExtensionBanner { - private initialized?: boolean; - private disabledInCurrentSession: boolean = false; - private sampleSizePerHundred: number; - private bannerMessage: string = 'Try out Preview of our new Python Language Server to get richer and faster IntelliSense completions, and syntax errors as you type.'; - private bannerLabels: string[] = [ 'Try it now', 'No thanks', 'Remind me Later' ]; - - constructor( - @inject(IApplicationShell) private appShell: IApplicationShell, - @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, - @inject(IConfigurationService) private configuration: IConfigurationService, - sampleSizePerOneHundredUsers: number = 10) - { - this.sampleSizePerHundred = sampleSizePerOneHundredUsers; - this.initialize(); - } - - public initialize() { - if (this.initialized) { - return; - } - this.initialized = true; - - // Don't even bother adding handlers if banner has been turned off. - if (!this.enabled) { - return; - } - - // we only want 10% of folks that use Jedi to see this survey. - const randomSample: number = getRandomBetween(0, 100); - if (randomSample >= this.sampleSizePerHundred) { - this.disable().ignoreErrors(); - return; - } - } - - public get shownCount(): Promise<number> { - return Promise.resolve(-1); // we don't count this popup banner! - } - - public get optionLabels(): string[] { - return this.bannerLabels; - } - - public get enabled(): boolean { - return this.persistentState.createGlobalPersistentState<boolean>(ProposeLSStateKeys.ShowBanner, true).value; - } - - public async showBanner(): Promise<void> { - if (!this.enabled) { - return; - } - - const show = await this.shouldShowBanner(); - if (!show) { - return; - } - - const response = await this.appShell.showInformationMessage(this.bannerMessage, ...this.bannerLabels); - switch (response) { - case this.bannerLabels[ProposeLSLabelIndex.Yes]: { - await this.enableNewLanguageServer(); - await this.disable(); - break; - } - case this.bannerLabels[ProposeLSLabelIndex.No]: { - await this.disable(); - break; - } - case this.bannerLabels[ProposeLSLabelIndex.Later]: { - this.disabledInCurrentSession = true; - break; - } - default: { - // Disable for the current session. - this.disabledInCurrentSession = true; - } - } - } - - public async shouldShowBanner(): Promise<boolean> { - return Promise.resolve(this.enabled && !this.disabledInCurrentSession); - } - - public async disable(): Promise<void> { - await this.persistentState.createGlobalPersistentState<boolean>(ProposeLSStateKeys.ShowBanner, false).updateValue(false); - } - - public async enableNewLanguageServer(): Promise<void> { - await this.configuration.updateSetting('jediEnabled', false, undefined, ConfigurationTarget.Global); - } -} diff --git a/src/client/linters/bandit.ts b/src/client/linters/bandit.ts deleted file mode 100644 index cf50e6053e67..000000000000 --- a/src/client/linters/bandit.ts +++ /dev/null @@ -1,33 +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'; - -export class Bandit extends BaseLinter { - constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { - super(Product.bandit, outputChannel, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise<ILintMessage[]> { - // 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 = { - LOW: LintMessageSeverity.Information, - MEDIUM: LintMessageSeverity.Warning, - HIGH: LintMessageSeverity.Error - }[msg.type]; - }); - return messages; - } -} diff --git a/src/client/linters/baseLinter.ts b/src/client/linters/baseLinter.ts deleted file mode 100644 index ba8f4b59ab9c..000000000000 --- a/src/client/linters/baseLinter.ts +++ /dev/null @@ -1,185 +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 '../common/extensions'; -import { IPythonToolExecutionService } from '../common/process/types'; -import { ExecutionInfo, IConfigurationService, ILogger, 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 -const namedRegexp = require('named-js-regexp'); -// Allow negative column numbers (https://github.com/PyCQA/pylint/issues/1822) -const REGEX = '(?<line>\\d+),(?<column>-?\\d+),(?<type>\\w+),(?<code>\\w\\d+):(?<message>.*)\\r?(\\n|$)'; - -export interface IRegexGroup { - line: number; - column: number; - code: string; - message: string; - type: string; -} - -export function matchNamedRegEx(data, regex): IRegexGroup | undefined { - const compiledRegexp = namedRegexp(regex, 'g'); - const rawMatch = compiledRegexp.exec(data); - if (rawMatch !== null) { - return <IRegexGroup>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(<any>match.line); - // tslint:disable-next-line:no-any - match.column = Number(<any>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>(ILinterManager).getLinterInfo(product); - this.errorHandler = new ErrorHandler(this.info.product, outputChannel, serviceContainer); - this.configService = serviceContainer.get<IConfigurationService>(IConfigurationService); - this.workspace = serviceContainer.get<IWorkspaceService>(IWorkspaceService); - } - - public get info(): ILinterInfo { - return this._info; - } - - public async lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise<ILintMessage[]> { - 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 get logger(): ILogger { - return this.serviceContainer.get<ILogger>(ILogger); - } - protected abstract runLinter(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise<ILintMessage[]>; - - // 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><any>LintMessageSeverity[severityName]; - } - } - } - } - return LintMessageSeverity.Information; - } - - protected async run(args: string[], document: vscode.TextDocument, cancellation: vscode.CancellationToken, regEx: string = REGEX): Promise<ILintMessage[]> { - 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>(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) { - 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 handleError(error: Error, resource: vscode.Uri, execInfo: ExecutionInfo) { - this.errorHandler.handleError(error, resource, execInfo) - .catch(this.logger.logError.bind(this, 'Error in errorHandler.handleError')); - } - - 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) { - this.logger.logError(`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/errorHandlers/baseErrorHandler.ts b/src/client/linters/errorHandlers/baseErrorHandler.ts deleted file mode 100644 index 994b6c0f0160..000000000000 --- a/src/client/linters/errorHandlers/baseErrorHandler.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { OutputChannel, Uri } from 'vscode'; -import { ExecutionInfo, IInstaller, ILogger, Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { IErrorHandler } from '../types'; - -export abstract class BaseErrorHandler implements IErrorHandler { - protected logger: ILogger; - protected installer: IInstaller; - - private handler: IErrorHandler; - - constructor(protected product: Product, protected outputChannel: OutputChannel, protected serviceContainer: IServiceContainer) { - this.logger = this.serviceContainer.get<ILogger>(ILogger); - this.installer = this.serviceContainer.get<IInstaller>(IInstaller); - } - protected get nextHandler() { - return this.handler; - } - public setNextHandler(handler: IErrorHandler): void { - this.handler = handler; - } - public abstract handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise<boolean>; -} diff --git a/src/client/linters/errorHandlers/errorHandler.ts b/src/client/linters/errorHandlers/errorHandler.ts deleted file mode 100644 index 5c2311bd8176..000000000000 --- a/src/client/linters/errorHandlers/errorHandler.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { OutputChannel, Uri } from 'vscode'; -import { ExecutionInfo, IInstaller, ILogger, Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { IErrorHandler, ILinterInfo } 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<boolean> { - 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 c7c56c66e7a9..000000000000 --- a/src/client/linters/errorHandlers/notInstalled.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { OutputChannel, Uri } from 'vscode'; -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<boolean> { - const pythonExecutionService = await this.serviceContainer.get<IPythonExecutionFactory>(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(this.logger.logError.bind(this, 'NotInstalledErrorHandler.promptToInstall')); - - const linterManager = this.serviceContainer.get<ILinterManager>(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}`); - this.logger.logWarning(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 3db392bd0c3a..000000000000 --- a/src/client/linters/errorHandlers/standard.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { OutputChannel, Uri, window } from 'vscode'; -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<boolean> { - 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>(ILinterManager); - const info = linterManager.getLinterInfo(execInfo.product!); - - this.logger.logError(`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, resource); - return true; - } - private async displayLinterError(linterId: LinterId, resource: Uri) { - const message = `There was an error in running the linter '${linterId}'`; - await window.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 d2c000a47fb0..000000000000 --- a/src/client/linters/flake8.ts +++ /dev/null @@ -1,22 +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<ILintMessage[]> { - 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); - }); - return messages; - } -} diff --git a/src/client/linters/linterAvailability.ts b/src/client/linters/linterAvailability.ts deleted file mode 100644 index 982508c7de69..000000000000 --- a/src/client/linters/linterAvailability.ts +++ /dev/null @@ -1,131 +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, IWorkspaceService } from '../common/application/types'; -import '../common/extensions'; -import { traceError } from '../common/logger'; -import { IConfigurationService, IInstaller, Product } from '../common/types'; -import { Linters } from '../common/utils/localize'; -import { IAvailableLinterActivator, ILinterInfo } from './types'; - -@injectable() -export class AvailableLinterActivator implements IAvailableLinterActivator { - constructor( - @inject(IApplicationShell) private appShell: IApplicationShell, - @inject(IInstaller) private installer: IInstaller, - @inject(IWorkspaceService) private workspaceConfig: IWorkspaceService, - @inject(IConfigurationService) private configService: IConfigurationService - ) { } - - /** - * 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<boolean> { - // 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.product, 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<boolean> { - type ConfigureLinterMessage = { - enabled: boolean; - title: string; - }; - - const optButtons: ConfigureLinterMessage[] = [ - { - title: `Enable ${linterInfo.id}`, - enabled: true - }, - { - title: `Disable ${linterInfo.id}`, - enabled: false - } - ]; - - // tslint:disable-next-line:messages-must-be-localized - const pick = await this.appShell.showInformationMessage(Linters.installedButNotEnabled().format(linterInfo.id), ...optButtons); - if (pick) { - await linterInfo.enableAsync(pick.enabled); - return true; - } - - return false; - } - - /** - * Check if the linter itself is available in the workspace's Python environment or - * not. - * - * @param linterProduct Linter to check in the current workspace environment. - * @param resource Context information for workspace. - */ - public async isLinterAvailable(linterProduct: Product, resource?: Uri): Promise<boolean | undefined> { - return this.installer.isInstalled(linterProduct, resource) - .catch((reason) => { - // report and continue, assume the linter is unavailable. - traceError(`[WARNING]: Failed to discover if linter ${linterProduct} is installed.`, reason); - return false; - }); - } - - /** - * 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.workspaceConfig.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.jediEnabled": false` enables it. - * - * @returns true if the global default for python.jediEnabled is false. - */ - public get isFeatureEnabled(): boolean { - return !this.configService.getSettings().jediEnabled; - } -} diff --git a/src/client/linters/linterCommands.ts b/src/client/linters/linterCommands.ts deleted file mode 100644 index 3c2a6dcfd664..000000000000 --- a/src/client/linters/linterCommands.ts +++ /dev/null @@ -1,104 +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 { SELECT_LINTER } 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>(ILinterManager); - this.appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell); - this.documentManager = this.serviceContainer.get<IDocumentManager>(IDocumentManager); - - const commandManager = this.serviceContainer.get<ICommandManager>(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<void> { - 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(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(SELECT_LINTER, undefined, {tool: selection as LinterId, enabled: true}); - } - } - } - - public async enableLintingAsync(): Promise<void> { - 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<DiagnosticCollection> { - const engine = this.serviceContainer.get<ILintingEngine>(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 d0c4368e5cac..000000000000 --- a/src/client/linters/linterInfo.ts +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as path from 'path'; -import { Uri } from 'vscode'; -import { IWorkspaceService } from '../common/application/types'; -import { ExecutionInfo, IConfigurationService, Product } from '../common/types'; -import { ILinterInfo, LinterId } from './types'; - -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<void> { - return this.configService.updateSetting(`linting.${this.enabledSettingName}`, enabled, resource); - } - public isEnabled(resource?: Uri): boolean { - const settings = this.configService.getSettings(resource); - return settings.linting[this.enabledSettingName] as boolean; - } - - public pathName(resource?: Uri): string { - const settings = this.configService.getSettings(resource); - return settings.linting[this.pathSettingName] as string; - } - public linterArgs(resource?: Uri): string[] { - const settings = this.configService.getSettings(resource); - const args = settings.linting[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, 'pylint', configService, configFileNames); - } - public isEnabled(resource?: Uri): boolean { - const enabled = super.isEnabled(resource); - if (!enabled || this.configService.getSettings(resource).jediEnabled) { - return enabled; - } - // If we're using new LS, then by default Pylint is disabled (unless the user provides a value). - const inspection = this.workspaceService.getConfiguration('python', resource).inspect<boolean>('linting.pylintEnabled'); - 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 1b8620fb9fc3..000000000000 --- a/src/client/linters/linterManager.ts +++ /dev/null @@ -1,158 +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 { - IConfigurationService, ILogger, 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 { Pep8 } from './pep8'; -import { Prospector } from './prospector'; -import { PyDocStyle } from './pydocstyle'; -import { PyLama } from './pylama'; -import { Pylint } from './pylint'; -import { - IAvailableLinterActivator, ILinter, ILinterInfo, ILinterManager, ILintMessage -} from './types'; - -class DisabledLinter implements ILinter { - constructor(private configService: IConfigurationService) { } - public get info() { - return new LinterInfo(Product.pylint, 'pylint', this.configService); - } - public async lint(document: TextDocument, cancellation: CancellationToken): Promise<ILintMessage[]> { - return []; - } -} - -@injectable() -export class LinterManager implements ILinterManager { - protected linters: ILinterInfo[]; - private configService: IConfigurationService; - private checkedForInstalledLinters = new Set<string>(); - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) { - this.configService = serviceContainer.get<IConfigurationService>(IConfigurationService); - this.linters = [ - new LinterInfo(Product.bandit, 'bandit', this.configService), - new LinterInfo(Product.flake8, 'flake8', this.configService), - new PylintLinterInfo(this.configService, this.workspaceService, ['.pylintrc', 'pylintrc']), - new LinterInfo(Product.mypy, 'mypy', this.configService), - new LinterInfo(Product.pep8, 'pep8', this.configService), - new LinterInfo(Product.prospector, 'prospector', this.configService), - new LinterInfo(Product.pydocstyle, 'pydocstyle', this.configService), - new LinterInfo(Product.pylama, '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'); - } - - public async isLintingEnabled(silent: boolean, resource?: Uri): Promise<boolean> { - 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<void> { - await this.configService.updateSetting('linting.enabled', enable, resource); - } - - public async getActiveLinters(silent: boolean, resource?: Uri): Promise<ILinterInfo[]> { - if (!silent) { - await this.enableUnconfiguredLinters(resource); - } - return this.linters.filter(x => x.isEnabled(resource)); - } - - public async setActiveLintersAsync(products: Product[], resource?: Uri): Promise<void> { - // 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<ILinter> { - 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.pep8: - return new Pep8(outputChannel, serviceContainer); - default: - serviceContainer.get<ILogger>(ILogger).logError(error); - break; - } - throw new Error(error); - } - - protected async enableUnconfiguredLinters(resource?: Uri): Promise<void> { - 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>(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 066080250e7d..000000000000 --- a/src/client/linters/lintingEngine.ts +++ /dev/null @@ -1,209 +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 { LinterErrors, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; -import { IFileSystem } from '../common/platform/types'; -import { IConfigurationService, IOutputChannel } from '../common/types'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { JupyterProvider } from '../jupyter/provider'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { LINTING } 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<LintMessageSeverity, vscode.DiagnosticSeverity>(); -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); - -// tslint:disable-next-line:interface-name -interface DocumentHasJupyterCodeCells { - // tslint:disable-next-line:callable-types - (doc: vscode.TextDocument, token: vscode.CancellationToken): Promise<Boolean>; -} - -@injectable() -export class LintingEngine implements ILintingEngine { - private documentHasJupyterCodeCells: DocumentHasJupyterCodeCells; - private workspace: IWorkspaceService; - private documents: IDocumentManager; - private configurationService: IConfigurationService; - private linterManager: ILinterManager; - private diagnosticCollection: vscode.DiagnosticCollection; - private pendingLintings = new Map<string, vscode.CancellationTokenSource>(); - private outputChannel: vscode.OutputChannel; - private fileSystem: IFileSystem; - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.documentHasJupyterCodeCells = (a, b) => Promise.resolve(false); - this.documents = serviceContainer.get<IDocumentManager>(IDocumentManager); - this.workspace = serviceContainer.get<IWorkspaceService>(IWorkspaceService); - this.configurationService = serviceContainer.get<IConfigurationService>(IConfigurationService); - this.outputChannel = serviceContainer.get<vscode.OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.linterManager = serviceContainer.get<ILinterManager>(ILinterManager); - this.fileSystem = serviceContainer.get<IFileSystem>(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<vscode.DiagnosticCollection> { - 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<void> { - 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<ILintMessage[]>[] = 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; - }); - - const hasJupyterCodeCells = await this.documentHasJupyterCodeCells(document, cancelToken.token); - // 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) { - // Ignore magic commands from jupyter. - if (hasJupyterCodeCells && document.lineAt(m.line - 1).text.trim().startsWith('%') && - (m.code === LinterErrors.pylint.InvalidSyntax || - m.code === LinterErrors.prospector.InvalidSyntax || - m.code === LinterErrors.flake8.InvalidSyntax)) { - continue; - } - 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); - } - - // tslint:disable-next-line:no-any - public async linkJupyterExtension(jupyter: vscode.Extension<any> | undefined): Promise<void> { - if (!jupyter) { - return; - } - if (!jupyter.isActive) { - await jupyter.activate(); - } - // tslint:disable-next-line:no-unsafe-any - jupyter.exports.registerLanguageProvider(PYTHON.language, new JupyterProvider()); - // tslint:disable-next-line:no-unsafe-any - this.documentHasJupyterCodeCells = jupyter.exports.hasCodeCells; - } - - private sendLinterRunTelemetry(info: ILinterInfo, resource: vscode.Uri, promise: Promise<ILintMessage[]>, 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(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<boolean> { - 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); - const ignoreMinmatches = settings.linting.ignorePatterns.map(pattern => new Minimatch(pattern)); - 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 7c8559881a42..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 = '(?<file>[^:]+):(?<line>\\d+)(:(?<column>\\d+))?: (?<type>\\w+): (?<message>.*)\\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<ILintMessage[]> { - 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/pep8.ts b/src/client/linters/pep8.ts deleted file mode 100644 index 959923c6ad5e..000000000000 --- a/src/client/linters/pep8.ts +++ /dev/null @@ -1,22 +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 Pep8 extends BaseLinter { - constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { - super(Product.pep8, outputChannel, serviceContainer, COLUMN_OFF_SET); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise<ILintMessage[]> { - 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.pep8CategorySeverity); - }); - return messages; - } -} diff --git a/src/client/linters/prospector.ts b/src/client/linters/prospector.ts deleted file mode 100644 index 0bd9873d5b8a..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 { 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<ILintMessage[]> { - 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); - this.logger.logError('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/pydocstyle.ts b/src/client/linters/pydocstyle.ts deleted file mode 100644 index c0812868b0e1..000000000000 --- a/src/client/linters/pydocstyle.ts +++ /dev/null @@ -1,81 +0,0 @@ -import * as path from 'path'; -import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; -import '../common/extensions'; -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<ILintMessage[]> { - 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) { - this.logger.logError(`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 edee2b44898f..000000000000 --- a/src/client/linters/pylama.ts +++ /dev/null @@ -1,25 +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 = '(?<file>.py):(?<line>\\d+):(?<column>\\d+): \\[(?<type>\\w+)\\] (?<code>\\w\\d+):? (?<message>.*)\\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<ILintMessage[]> { - 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 250b8a182647..000000000000 --- a/src/client/linters/pylint.ts +++ /dev/null @@ -1,151 +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 = '(?<line>\\d+),(?<column>-?\\d+),(?<type>\\w+),(?<code>[\\w-]+):(?<message>.*)\\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>(IFileSystem); - this.platformService = serviceContainer.get<IPlatformService>(IPlatformService); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise<ILintMessage[]> { - 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.hasConfigrationFileInWorkspace(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, this.pythonSettings.linting.pylintCategorySeverity); - }); - - return messages; - } - - // tslint:disable-next-line:member-ordering - public static async hasConfigurationFile(fs: IFileSystem, folder: string, platformService: IPlatformService): Promise<boolean> { - // 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 hasConfigrationFileInWorkspace(fs: IFileSystem, folder: string, root: string): Promise<boolean> { - // 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 b88ed789a9be..000000000000 --- a/src/client/linters/serviceRegistry.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import { IServiceManager } from '../ioc/types'; -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>(ILintingEngine, LintingEngine); - serviceManager.addSingleton<ILinterManager>(ILinterManager, LinterManager); - serviceManager.add<IAvailableLinterActivator>(IAvailableLinterActivator, AvailableLinterActivator); -} diff --git a/src/client/linters/types.ts b/src/client/linters/types.ts deleted file mode 100644 index 4a8bdea3690e..000000000000 --- a/src/client/linters/types.ts +++ /dev/null @@ -1,75 +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<boolean>; -} - -export type LinterId = 'flake8' | 'mypy' | 'pep8' | 'prospector' | 'pydocstyle' | 'pylama' | 'pylint' | '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<void>; - 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<ILintMessage[]>; -} - -export const IAvailableLinterActivator = Symbol('IAvailableLinterActivator'); -export interface IAvailableLinterActivator { - promptIfLinterAvailable(linter: ILinterInfo, resource?: vscode.Uri): Promise<boolean>; -} - -export const ILinterManager = Symbol('ILinterManager'); -export interface ILinterManager { - getAllLinterInfos(): ILinterInfo[]; - getLinterInfo(product: Product): ILinterInfo; - getActiveLinters(silent: boolean, resource?: vscode.Uri): Promise<ILinterInfo[]>; - isLintingEnabled(silent: boolean, resource?: vscode.Uri): Promise<boolean>; - enableLintingAsync(enable: boolean, resource?: vscode.Uri): Promise<void>; - setActiveLintersAsync(products: Product[], resource?: vscode.Uri): Promise<void>; - createLinter(product: Product, outputChannel: vscode.OutputChannel, serviceContainer: IServiceContainer, resource?: vscode.Uri): Promise<ILinter>; -} - -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<vscode.DiagnosticCollection>; - lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise<void>; - // tslint:disable-next-line:no-any - linkJupyterExtension(jupyter: vscode.Extension<any> | undefined): Promise<void>; - clearDiagnostics(document: vscode.TextDocument): void; -} 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/index.ts b/src/client/logging/index.ts new file mode 100644 index 000000000000..39d5652e100a --- /dev/null +++ b/src/client/logging/index.ts @@ -0,0 +1,215 @@ +// 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 */ + +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<any>) { + 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<T>(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<void>) + .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 = '<anon>'; + } + 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/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/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<any>, +) => TypedPropertyDescriptor<any>; + +// 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 new file mode 100644 index 000000000000..4229fd7976a5 --- /dev/null +++ b/src/client/logging/util.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Uri } from 'vscode'; + +export type Arguments = unknown[]; + +function valueToLogString(value: unknown, kind: string): string { + if (value === undefined) { + return 'undefined'; + } + if (value === null) { + return 'null'; + } + try { + if (value && (value as Uri).fsPath) { + return `<Uri:${(value as Uri).fsPath}>`; + } + return JSON.stringify(value); + } catch { + return `<${kind} cannot be serialized for logging>`; + } +} + +// Convert the given array of values (func call arguments) into a string +// suitable to be used in a log message. +export function argsToLogString(args: Arguments): string { + if (!args) { + return ''; + } + try { + const argStrings = args.map((item, index) => { + const valueString = valueToLogString(item, 'argument'); + return `Arg ${index + 1}: ${valueString}`; + }); + return argStrings.join(', '); + } catch { + return ''; + } +} + +// Convert the given return value into a string +// suitable to be used in a log message. +export function returnValueToLogString(returnValue: unknown): string { + const valueString = valueToLogString(returnValue, 'Return value'); + return `Return Value: ${valueString}`; +} + +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 new file mode 100644 index 000000000000..e90e6ea97fd2 --- /dev/null +++ b/src/client/providers/codeActionProvider/launchJsonCodeActionProvider.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CodeAction, + CodeActionContext, + CodeActionKind, + CodeActionProvider, + Diagnostic, + Range, + TextDocument, + WorkspaceEdit, +} from 'vscode'; + +/** + * Provides code actions for launch.json + */ +export class LaunchJsonCodeActionProvider implements CodeActionProvider { + public provideCodeActions(document: TextDocument, _: Range, context: CodeActionContext): CodeAction[] { + return context.diagnostics + .filter((diagnostic) => diagnostic.message === 'Incorrect type. Expected "string".') + .map((diagnostic) => this.createFix(document, diagnostic)); + } + + // 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); + fix.edit = new WorkspaceEdit(); + fix.edit.replace(document.uri, diagnostic.range, finalText); + return fix; + } +} diff --git a/src/client/providers/codeActionProvider/main.ts b/src/client/providers/codeActionProvider/main.ts new file mode 100644 index 000000000000..259f42848606 --- /dev/null +++ b/src/client/providers/codeActionProvider/main.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as vscodeTypes from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IDisposableRegistry } from '../../common/types'; +import { LaunchJsonCodeActionProvider } from './launchJsonCodeActionProvider'; + +@injectable() +export class CodeActionProviderService implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor(@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry) {} + + public async activate(): Promise<void> { + // eslint-disable-next-line global-require + const vscode = require('vscode') as typeof vscodeTypes; + const documentSelector: vscodeTypes.DocumentFilter = { + scheme: 'file', + language: 'jsonc', + pattern: '**/launch.json', + }; + this.disposableRegistry.push( + vscode.languages.registerCodeActionsProvider(documentSelector, new LaunchJsonCodeActionProvider(), { + providedCodeActionKinds: [vscode.CodeActionKind.QuickFix], + }), + ); + } +} diff --git a/src/client/providers/codeActionsProvider.ts b/src/client/providers/codeActionsProvider.ts deleted file mode 100644 index cd113223b819..000000000000 --- a/src/client/providers/codeActionsProvider.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as vscode from 'vscode'; - -export class PythonCodeActionProvider implements vscode.CodeActionProvider { - public provideCodeActions(document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeAction[]> { - 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 89c89f732091..000000000000 --- a/src/client/providers/completionProvider.ts +++ /dev/null @@ -1,42 +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 { COMPLETION } 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>(IConfigurationService); - } - - @captureTelemetry(COMPLETION) - public async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): - Promise<vscode.CompletionItem[]> { - 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<vscode.CompletionItem> { - 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 c287d4fa858c..000000000000 --- a/src/client/providers/completionSource.ts +++ /dev/null @@ -1,106 +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<vscode.CompletionItem[]> { - 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<LanguageItemInfo[] | undefined> { - 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<proxy.ICompletionResult | undefined> { - 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<proxy.ICommandResult> = { - command: type, - fileName: document.fileName, - columnIndex: columnIndex, - lineIndex: position.line, - source: source - }; - - return this.jediFactory.getJediProxyHandler<proxy.ICompletionResult>(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>(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 f1555e58b9fc..000000000000 --- a/src/client/providers/definitionProvider.ts +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -import * as vscode from 'vscode'; -import { JediFactory } from '../languageServices/jediProxyFactory'; -import { captureTelemetry } from '../telemetry'; -import { DEFINITION } 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(DEFINITION) - public async provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | undefined> { - 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<proxy.IDefinitionResult> = { - 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<proxy.IDefinitionResult>(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 2b163cade5d1..000000000000 --- a/src/client/providers/docStringFoldingProvider.ts +++ /dev/null @@ -1,107 +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<FoldingRange[]> { - 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 9398fd7b0e58..000000000000 --- a/src/client/providers/formatProvider.ts +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { setTimeout } from 'timers'; -import * as vscode from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { IConfigurationService } from '../common/types'; -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<string, BaseFormatter>(); - 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>(ICommandManager); - this.workspace = serviceContainer.get<IWorkspaceService>(IWorkspaceService); - this.documentManager = serviceContainer.get<IDocumentManager>(IDocumentManager); - this.config = serviceContainer.get<IConfigurationService>(IConfigurationService); - this.disposables.push(this.documentManager.onDidSaveTextDocument(async document => this.onSaveDocument(document))); - } - - public dispose() { - this.disposables.forEach(d => d.dispose()); - } - - public provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): Promise<vscode.TextEdit[]> { - return this.provideDocumentRangeFormattingEdits(document, undefined, options, token); - } - - public async provideDocumentRangeFormattingEdits(document: vscode.TextDocument, range: vscode.Range | undefined, options: vscode.FormattingOptions, token: vscode.CancellationToken): Promise<vscode.TextEdit[]> { - // 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<void> { - // 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 745300986189..000000000000 --- a/src/client/providers/hoverProvider.ts +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -import * as vscode from 'vscode'; -import { JediFactory } from '../languageServices/jediProxyFactory'; -import { captureTelemetry } from '../telemetry'; -import { HOVER_DEFINITION } 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(HOVER_DEFINITION) - public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) - : Promise<vscode.Hover | undefined> { - 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 394ec631f853..000000000000 --- a/src/client/providers/importSortProvider.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { inject, injectable } from 'inversify'; -import { EOL } from 'os'; -import * as path from 'path'; -import { CancellationToken, Uri, WorkspaceEdit } from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; -import { Commands, EXTENSION_ROOT_DIR, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; -import { IFileSystem } from '../common/platform/types'; -import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; -import { IConfigurationService, IDisposableRegistry, IEditorUtils, ILogger, IOutputChannel } from '../common/types'; -import { noop } from '../common/utils/misc'; -import { IServiceContainer } from '../ioc/types'; -import { captureTelemetry } from '../telemetry'; -import { FORMAT_SORT_IMPORTS } from '../telemetry/constants'; -import { ISortImportsEditingProvider } from './types'; - -@injectable() -export class SortImportsEditingProvider implements ISortImportsEditingProvider { - 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>(IApplicationShell); - this.documentManager = serviceContainer.get<IDocumentManager>(IDocumentManager); - this.configurationService = serviceContainer.get<IConfigurationService>(IConfigurationService); - this.pythonExecutionFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); - this.processServiceFactory = serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory); - this.editorUtils = serviceContainer.get<IEditorUtils>(IEditorUtils); - } - @captureTelemetry(FORMAT_SORT_IMPORTS) - public async provideDocumentSortImportsEdits(uri: Uri, token?: CancellationToken): Promise<WorkspaceEdit | undefined> { - const document = await this.documentManager.openTextDocument(uri); - if (!document) { - return; - } - if (document.lineCount <= 1) { - return; - } - // isort does 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. - // Yes 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. - const importScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'sortImports.py'); - const fsService = this.serviceContainer.get<IFileSystem>(IFileSystem); - const tmpFile = document.isDirty ? await fsService.createTemporaryFile(path.extname(document.uri.fsPath)) : undefined; - if (tmpFile) { - await fsService.writeFile(tmpFile.filePath, document.getText()); - } - const settings = this.configurationService.getSettings(uri); - const isort = settings.sortImports.path; - const filePath = tmpFile ? tmpFile.filePath : document.uri.fsPath; - const args = [filePath, '--diff'].concat(settings.sortImports.args); - let diffPatch: string; - - if (token && token.isCancellationRequested) { - return; - } - try { - if (typeof isort === 'string' && isort.length > 0) { - // Lets just treat this as a standard tool. - const processService = await this.processServiceFactory.create(document.uri); - diffPatch = (await processService.exec(isort, args, { throwOnStdErr: true, token })).stdout; - } else { - const processExeService = await this.pythonExecutionFactory.create({ resource: document.uri }); - diffPatch = (await processExeService.exec([importScript].concat(args), { throwOnStdErr: true, token })).stdout; - } - - return this.editorUtils.getWorkspaceEditsFromPatch(document.getText(), diffPatch, document.uri); - } finally { - if (tmpFile) { - tmpFile.dispose(); - } - } - } - - public registerCommands() { - const cmdManager = this.serviceContainer.get<ICommandManager>(ICommandManager); - const disposable = cmdManager.registerCommand(Commands.Sort_Imports, this.sortImports, this); - this.serviceContainer.get<IDisposableRegistry>(IDisposableRegistry).push(disposable); - } - public async sortImports(uri?: Uri): Promise<void> { - 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>(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - outputChannel.appendLine(error); - outputChannel.show(); - const logger = this.serviceContainer.get<ILogger>(ILogger); - logger.logError(`Failed to format imports for '${uri.fsPath}'.`, error); - this.shell.showErrorMessage(message).then(noop, noop); - } - } -} diff --git a/src/client/providers/itemInfoSource.ts b/src/client/providers/itemInfoSource.ts deleted file mode 100644 index 5effbe7483e4..000000000000 --- a/src/client/providers/itemInfoSource.ts +++ /dev/null @@ -1,169 +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<LanguageItemInfo[] | undefined>; - getItemInfoFromDocument(document: vscode.TextDocument, position: vscode.Position, - token: vscode.CancellationToken): Promise<LanguageItemInfo[] | undefined>; -} - -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<LanguageItemInfo[] | undefined> { - 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<LanguageItemInfo[] | undefined> { - 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<proxy.IHoverResult | undefined> { - 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<proxy.IHoverResult | undefined> { - const cmd: proxy.ICommand<proxy.IHoverResult> = { - 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<proxy.IHoverResult>(document.uri).sendCommand(cmd, token); - } - - private async getHoverResultFromTextRange(documentUri: vscode.Uri, fileName: string, range: vscode.Range, sourceText: string, token: vscode.CancellationToken) - : Promise<proxy.IHoverResult | undefined> { - const cmd: proxy.ICommand<proxy.IHoverResult> = { - command: proxy.CommandType.Hover, - fileName: fileName, - columnIndex: range.end.character, - lineIndex: range.end.line, - source: sourceText - }; - return this.jediFactory.getJediProxyHandler<proxy.IHoverResult>(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 69a38a01023f..000000000000 --- a/src/client/providers/jediProxy.ts +++ /dev/null @@ -1,872 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// tslint:disable-next-line:no-var-requires no-require-imports -import { ChildProcess } from 'child_process'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import * as pidusage from 'pidusage'; -import { - CancellationToken, CancellationTokenSource, CompletionItemKind, - Disposable, SymbolKind, Uri -} from 'vscode'; -import { isTestExecution } from '../common/constants'; -import '../common/extensions'; -import { IS_WINDOWS } from '../common/platform/constants'; -import { IPythonExecutionFactory } from '../common/process/types'; -import { BANNER_NAME_PROPOSE_LS, IConfigurationService, ILogger, IPythonExtensionBanner, IPythonSettings } from '../common/types'; -import { createDeferred, Deferred } from '../common/utils/async'; -import { debounce, swallowExceptions } from '../common/utils/decorators'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IEnvironmentVariablesProvider } from '../common/variables/types'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { Logger } from './../common/logger'; - -const pythonVSCodeTypeMappings = new Map<string, CompletionItemKind>(); -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<string, SymbolKind>(); -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<CommandType, string>(); -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<number, IExecutionCommand<ICommandResult>>(); - private commandQueue: number[] = []; - private spawnRetryAttempts = 0; - private additionalAutoCompletePaths: string[] = []; - private workspacePath: string; - private languageServerStarted!: Deferred<void>; - private initialized: Deferred<void>; - private environmentVariablesProvider!: IEnvironmentVariablesProvider; - private logger: ILogger; - private ignoreJediMemoryFootprint: boolean = false; - private pidUsageFailures = { timer: new StopWatch(), counter: 0 }; - private lastCmdIdProcessed?: number; - private lastCmdIdProcessedForPidUsage?: number; - private proposeNewLanguageServerPopup: IPythonExtensionBanner; - private readonly disposables: Disposable[] = []; - - public constructor(private extensionRootDir: string, workspacePath: string, private serviceContainer: IServiceContainer) { - this.workspacePath = workspacePath; - const configurationService = serviceContainer.get<IConfigurationService>(IConfigurationService); - this.pythonSettings = configurationService.getSettings(Uri.file(workspacePath)); - this.lastKnownPythonInterpreter = this.pythonSettings.pythonPath; - this.logger = serviceContainer.get<ILogger>(ILogger); - const interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService); - const disposable = interpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter.bind(this)); - this.disposables.push(disposable); - this.initialized = createDeferred<void>(); - this.startLanguageServer().then(() => this.initialized.resolve()).ignoreErrors(); - - this.proposeNewLanguageServerPopup = serviceContainer.get<IPythonExtensionBanner>(IPythonExtensionBanner, BANNER_NAME_PROPOSE_LS); - - this.checkJediMemoryFootprint().ignoreErrors(); - } - - private static getProperty<T>(o: object, name: string): T { - return <T>o[name]; - } - - public dispose() { - while (this.disposables.length > 0) { - const disposable = this.disposables.pop(); - if (disposable) { - disposable.dispose(); - } - } - this.killProcess(); - } - - public getNextCommandId(): number { - const result = this.cmdId; - this.cmdId += 1; - return result; - } - - public async sendCommand<T extends ICommandResult>(cmd: ICommand<T>): Promise<T> { - await this.initialized.promise; - await this.languageServerStarted.promise; - if (!this.proc) { - return Promise.reject(new Error('Python proc not initialized')); - } - - const executionCmd = <IExecutionCommand<T>>cmd; - const payload = this.createPayload(executionCmd); - executionCmd.deferred = createDeferred<T>(); - try { - this.proc.stdin.write(`${JSON.stringify(payload)}\n`); - this.commands.set(executionCmd.id, executionCmd); - this.commandQueue.push(executionCmd.id); - } catch (ex) { - console.error(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<void> { - return this.spawnProcess(path.join(this.extensionRootDir, 'pythonFiles')) - .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(); - setTimeout(() => this.checkJediMemoryFootprint(), 15 * 1000); - } - private async checkJediMemoryFootprintImpl(): Promise<void> { - 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<void>(); - pidusage.stat(this.proc.pid, async (err, result) => { - 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(); - } - console.error('Python Extension: (pidusage)', err); - } else { - const limit = Math.min(Math.max(this.pythonSettings.jediMemoryLimit, 1024), 8192); - if (result && result.memory > limit * 1024 * 1024) { - this.logger.logWarning(`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; - } - - @swallowExceptions('JediProxy') - private async onDidChangeInterpreter() { - if (this.lastKnownPythonInterpreter === this.pythonSettings.pythonPath) { - return; - } - this.lastKnownPythonInterpreter = this.pythonSettings.pythonPath; - this.additionalAutoCompletePaths = await this.buildAutoCompletePaths(); - this.restartLanguageServer().ignoreErrors(); - } - @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<void> { - const newAutoComletePaths = await this.buildAutoCompletePaths(); - this.additionalAutoCompletePaths = newAutoComletePaths; - if (!isTestExecution()) { - await this.proposeNewLanguageServerPopup.showBanner(); - } - return this.restartLanguageServer(); - } - private restartLanguageServer(): Promise<void> { - 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) { - Logger.error(`${source} jediProxy`, `Error (${source}) ${errorMessage}`); - } - - // tslint:disable-next-line:max-func-body-length - private async spawnProcess(cwd: string) { - if (this.languageServerStarted && !this.languageServerStarted.completed) { - this.languageServerStarted.reject(new Error('Language Server not started.')); - } - this.languageServerStarted = createDeferred<void>(); - const pythonProcess = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource: Uri.file(this.workspacePath) }); - // Check if the python path is valid. - if ((await pythonProcess.getExecutablePath().catch(() => '')).length === 0) { - return; - } - const args = ['completion.py']; - if (typeof this.pythonSettings.jediPath === 'string' && this.pythonSettings.jediPath.length > 0) { - args.push('custom'); - args.push(this.pythonSettings.jediPath); - } - const result = pythonProcess.execObservable(args, { cwd }); - this.proc = result.proc; - this.languageServerStarted.resolve(); - this.proc!.on('end', (end) => { - Logger.error('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(cwd) - .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 = dataStr.splitLines().map(resp => JSON.parse(resp)); - 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<number>(response, 'id'); - if (!this.commands.has(responseId)) { - return; - } - const cmd = this.commands.get(responseId); - if (!cmd) { - return; - } - this.lastCmdIdProcessed = cmd.id; - if (JediProxy.getProperty<object>(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<ICommandResult>, 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<ICommandResult>, response: object): void { - let results = JediProxy.getProperty<IAutoCompleteItem[]>(response, 'results'); - results = Array.isArray(results) ? results : []; - results.forEach(item => { - // tslint:disable-next-line:no-any - const originalType = <string><any>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<ICommandResult>, response: object): void { - // tslint:disable-next-line:no-any - const defs = JediProxy.getProperty<any[]>(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<ICommandResult>, response: object): void { - // tslint:disable-next-line:no-any - const defs = JediProxy.getProperty<any[]>(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<ICommandResult>, response: object): void { - // tslint:disable-next-line:no-any - let defs = JediProxy.getProperty<any[]>(response, 'results'); - defs = Array.isArray(defs) ? defs : []; - const defResults: ISymbolResult = { - requestId: command.id, - definitions: [] - }; - defResults.definitions = defs.map<IDefinition>(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<ICommandResult>, response: object): void { - // tslint:disable-next-line:no-any - let defs = JediProxy.getProperty<any[]>(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<ICommandResult>, response: object): void { - // tslint:disable-next-line:no-any - const defs = JediProxy.getProperty<any[]>(response, 'results'); - // tslint:disable-next-line:no-object-literal-type-assertion - this.safeResolve(command, <IArgumentsResult>{ - 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<T extends ICommandResult>(cmd: IExecutionCommand<T>): 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 getPathFromPythonCommand(args: string[]): Promise<string> { - try { - const pythonProcess = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource: Uri.file(this.workspacePath) }); - const result = await pythonProcess.exec(args, { cwd: this.workspacePath }); - const lines = result.stdout.trim().splitLines(); - if (lines.length === 0) { - return ''; - } - const exists = await fs.pathExists(lines[0]); - return exists ? lines[0] : ''; - } catch { - return ''; - } - } - private async buildAutoCompletePaths(): Promise<string[]> { - const filePathPromises = [ - // Sysprefix. - this.getPathFromPythonCommand(['-c', 'import sys;print(sys.prefix)']).catch(() => ''), - // exeucutable path. - this.getPathFromPythonCommand(['-c', 'import sys;print(sys.executable)']).then(execPath => path.dirname(execPath)).catch(() => ''), - // Python specific site packages. - // 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())". - this.getPathFromPythonCommand(['-c', 'from distutils.sysconfig import get_python_lib; print(get_python_lib())']) - .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.getPathFromPythonCommand(['-m', 'site', '--user-site']).catch(() => '') - ]; - - try { - const pythonPaths = await this.getEnvironmentVariablesProvider().getEnvironmentVariables(Uri.file(this.workspacePath)) - .then(customEnvironmentVars => customEnvironmentVars ? JediProxy.getProperty<string>(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) { - console.error('Python Extension: jediProxy.filePaths', ex); - return []; - } - } - private getEnvironmentVariablesProvider() { - if (!this.environmentVariablesProvider) { - this.environmentVariablesProvider = this.serviceContainer.get<IEnvironmentVariablesProvider>(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<ICommandResult> | undefined | null, - result: ICommandResult | PromiseLike<ICommandResult> | undefined): void { - if (command && command.deferred) { - command.deferred.resolve(result); - } - } -} - -// tslint:disable-next-line:no-unused-variable -export interface ICommand<T extends ICommandResult> { - telemetryEvent?: string; - command: CommandType; - source?: string; - fileName: string; - lineIndex: number; - columnIndex: number; -} - -interface IExecutionCommand<T extends ICommandResult> extends ICommand<T> { - id: number; - deferred?: Deferred<T>; - 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<R extends ICommandResult> implements Disposable { - private commandCancellationTokenSources: Map<CommandType, CancellationTokenSource>; - - public get JediProxy(): JediProxy { - return this.jediProxy; - } - - public constructor(private jediProxy: JediProxy) { - this.commandCancellationTokenSources = new Map<CommandType, CancellationTokenSource>(); - } - - public dispose() { - if (this.jediProxy) { - this.jediProxy.dispose(); - } - } - - public sendCommand(cmd: ICommand<R>, token?: CancellationToken): Promise<R | undefined> { - const executionCmd = <IExecutionCommand<R>>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<R>(executionCmd) - .catch(reason => { - console.error(reason); - return undefined; - }); - } - - public sendCommandNonCancellableCommand(cmd: ICommand<R>, token?: CancellationToken): Promise<R | undefined> { - const executionCmd = <IExecutionCommand<R>>cmd; - executionCmd.id = executionCmd.id || this.jediProxy.getNextCommandId(); - if (token) { - executionCmd.token = token; - } - - return this.jediProxy.sendCommand<R>(executionCmd) - .catch(reason => { - console.error(reason); - return undefined; - }); - } -} diff --git a/src/client/providers/linterProvider.ts b/src/client/providers/linterProvider.ts deleted file mode 100644 index 0e13f02a124c..000000000000 --- a/src/client/providers/linterProvider.ts +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import { - ConfigurationChangeEvent, Disposable, - ExtensionContext, TextDocument, Uri, workspace -} from 'vscode'; -import { IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { isTestExecution } from '../common/constants'; -import '../common/extensions'; -import { IFileSystem } from '../common/platform/types'; -import { IConfigurationService } from '../common/types'; -import { IInterpreterService } from '../interpreter/contracts'; -import { IServiceContainer } from '../ioc/types'; -import { ILinterManager, ILintingEngine } from '../linters/types'; - -export class LinterProvider implements Disposable { - private context: ExtensionContext; - private disposables: Disposable[]; - private interpreterService: IInterpreterService; - private documents: IDocumentManager; - private configuration: IConfigurationService; - private linterManager: ILinterManager; - private engine: ILintingEngine; - private fs: IFileSystem; - private readonly workspaceService: IWorkspaceService; - - public constructor(context: ExtensionContext, serviceContainer: IServiceContainer) { - this.context = context; - this.disposables = []; - - this.fs = serviceContainer.get<IFileSystem>(IFileSystem); - this.engine = serviceContainer.get<ILintingEngine>(ILintingEngine); - this.linterManager = serviceContainer.get<ILinterManager>(ILinterManager); - this.interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService); - this.documents = serviceContainer.get<IDocumentManager>(IDocumentManager); - this.configuration = serviceContainer.get<IConfigurationService>(IConfigurationService); - this.workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService); - - this.disposables.push(this.interpreterService.onDidChangeInterpreter(() => this.engine.lintOpenPythonFiles())); - - this.documents.onDidOpenTextDocument(e => this.onDocumentOpened(e), this.context.subscriptions); - this.documents.onDidCloseTextDocument(e => this.onDocumentClosed(e), this.context.subscriptions); - this.documents.onDidSaveTextDocument(e => this.onDocumentSaved(e), this.context.subscriptions); - - 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()) { - setTimeout(() => this.engine.lintOpenPythonFiles().ignoreErrors(), 1200); - } - } - - 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 6742d2de9787..000000000000 --- a/src/client/providers/objectDefinitionProvider.ts +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; - -import * as vscode from 'vscode'; -import { JediFactory } from '../languageServices/jediProxyFactory'; -import { captureTelemetry } from '../telemetry'; -import { GO_TO_OBJECT_DEFINITION } from '../telemetry/constants'; -import * as defProvider from './definitionProvider'; - -export function activateGoToObjectDefinitionProvider(jediFactory: JediFactory): vscode.Disposable[] { - const def = new PythonObjectDefinitionProvider(jediFactory); - const commandRegistration = vscode.commands.registerCommand("python.goToPythonObject", () => def.goToObjectDefinition()); - return [def, commandRegistration] as vscode.Disposable[]; -} - -export class PythonObjectDefinitionProvider { - private readonly _defProvider: defProvider.PythonDefinitionProvider; - public constructor(jediFactory: JediFactory) { - this._defProvider = new defProvider.PythonDefinitionProvider(jediFactory); - } - - @captureTelemetry(GO_TO_OBJECT_DEFINITION) - public async goToObjectDefinition() { - let pathDef = await this.getObjectDefinition(); - if (typeof pathDef !== 'string' || pathDef.length === 0) { - return; - } - - let parts = pathDef.split('.'); - let source = ''; - let startColumn = 0; - if (parts.length === 1) { - source = `import ${parts[0]}`; - startColumn = 'import '.length; - } - else { - let 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); - let doc = <vscode.TextDocument><any>{ - fileName: 'test.py', - lineAt: (line: number) => { - return { text: source }; - }, - getWordRangeAtPosition: (position: vscode.Position) => range, - isDirty: true, - getText: () => source - }; - - let tokenSource = new vscode.CancellationTokenSource(); - let 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; - let lineNumber: number; - if (Array.isArray(defs) && defs.length > 0) { - uri = defs[0].uri; - lineNumber = defs[0].range.start.line; - } - if (!Array.isArray(defs) && defs.uri) { - uri = defs.uri; - lineNumber = defs.range.start.line; - } - - if (uri) { - let doc = await vscode.workspace.openTextDocument(uri); - await vscode.window.showTextDocument(doc); - 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<string> { - let value = await vscode.window.showInputBox({ prompt: "Enter Object Path", validateInput: this.intputValidation }); - return value; - } -} diff --git a/src/client/providers/providerUtilities.ts b/src/client/providers/providerUtilities.ts deleted file mode 100644 index 7ee45ab8e25a..000000000000 --- a/src/client/providers/providerUtilities.ts +++ /dev/null @@ -1,28 +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<IToken> { - 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 c57e35e0299b..000000000000 --- a/src/client/providers/referenceProvider.ts +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -import * as vscode from 'vscode'; -import { JediFactory } from '../languageServices/jediProxyFactory'; -import { captureTelemetry } from '../telemetry'; -import { REFERENCE } 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(REFERENCE) - public async provideReferences(document: vscode.TextDocument, position: vscode.Position, _context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise<vscode.Location[] | undefined> { - 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<proxy.IReferenceResult> = { - command: proxy.CommandType.Usages, - fileName: filename, - columnIndex: columnIndex, - lineIndex: position.line - }; - - if (document.isDirty) { - cmd.source = document.getText(); - } - - const data = await this.jediFactory.getJediProxyHandler<proxy.IReferenceResult>(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 adf9a62f9093..000000000000 --- a/src/client/providers/renameProvider.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - CancellationToken, OutputChannel, - Position, ProviderResult, RenameProvider, - TextDocument, window, workspace, WorkspaceEdit -} from 'vscode'; -import { EXTENSION_ROOT_DIR, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; -import { getWorkspaceEditsFromPatch } from '../common/editor'; -import { IConfigurationService, IInstaller, IOutputChannel, Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { RefactorProxy } from '../refactor/proxy'; -import { captureTelemetry } from '../telemetry'; -import { REFACTOR_RENAME } from '../telemetry/constants'; - -type RenameResponse = { - results: [{ diff: string }]; -}; - -export class PythonRenameProvider implements RenameProvider { - private readonly outputChannel: OutputChannel; - private readonly configurationService: IConfigurationService; - constructor(private serviceContainer: IServiceContainer) { - this.outputChannel = serviceContainer.get<OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.configurationService = serviceContainer.get<IConfigurationService>(IConfigurationService); - } - @captureTelemetry(REFACTOR_RENAME) - public provideRenameEdits(document: TextDocument, position: Position, newName: string, token: CancellationToken): ProviderResult<WorkspaceEdit> { - return workspace.saveAll(false).then(() => { - return this.doRename(document, position, newName, token); - }); - } - - private doRename(document: TextDocument, position: Position, newName: string, token: CancellationToken): ProviderResult<WorkspaceEdit> { - 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 pythonSettings = this.configurationService.getSettings(workspaceFolder ? workspaceFolder.uri : undefined); - - const proxy = new RefactorProxy(EXTENSION_ROOT_DIR, pythonSettings, workspaceRoot, this.serviceContainer); - return proxy.rename<RenameResponse>(document, newName, document.uri.fsPath, range).then(response => { - const fileDiffs = response.results.map(fileChanges => fileChanges.diff); - return getWorkspaceEditsFromPatch(fileDiffs, workspaceRoot); - }).catch(reason => { - if (reason === 'Not installed') { - const installer = this.serviceContainer.get<IInstaller>(IInstaller); - installer.promptToInstall(Product.rope, document.uri) - .catch(ex => console.error('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 ddde0fea33ce..dd9df89a78a3 100644 --- a/src/client/providers/replProvider.ts +++ b/src/client/providers/replProvider.ts @@ -1,38 +1,43 @@ -import { Disposable, Uri } from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; +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 { REPL } 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>(IActiveResourceService); this.registerCommand(); } - public dispose() { - this.disposables.forEach(disposable => disposable.dispose()); + + public dispose(): void { + this.disposables.forEach((disposable) => disposable.dispose()); } + private registerCommand() { const commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager); const disposable = commandManager.registerCommand(Commands.Start_REPL, this.commandHandler, this); this.disposables.push(disposable); } - @captureTelemetry(REPL) + private async commandHandler() { - const resource = this.getActiveResourceUri(); - const replProvider = this.serviceContainer.get<ICodeExecutionService>(ICodeExecutionService, 'repl'); - await replProvider.initializeRepl(resource); - } - private getActiveResourceUri(): Uri | undefined { - const documentManager = this.serviceContainer.get<IDocumentManager>(IDocumentManager); - if (documentManager.activeTextEditor && !documentManager.activeTextEditor!.document.isUntitled) { - return documentManager.activeTextEditor!.document.uri; - } - const workspace = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { - return workspace.workspaceFolders[0].uri; + const resource = this.activeResourceService.getActiveResource(); + const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(resource); + if (!interpreter) { + this.serviceContainer + .get<ICommandManager>(ICommandManager) + .executeCommand(Commands.TriggerEnvironmentSelection, resource) + .then(noop, noop); + return; } + const replProvider = this.serviceContainer.get<ICodeExecutionService>(ICodeExecutionService, 'standard'); + await replProvider.initializeRepl(resource); } } diff --git a/src/client/providers/serviceRegistry.ts b/src/client/providers/serviceRegistry.ts index 7418e0175e51..a96ec14ff5e9 100644 --- a/src/client/providers/serviceRegistry.ts +++ b/src/client/providers/serviceRegistry.ts @@ -3,10 +3,13 @@ 'use strict'; +import { IExtensionSingleActivationService } from '../activation/types'; import { IServiceManager } from '../ioc/types'; -import { SortImportsEditingProvider } from './importSortProvider'; -import { ISortImportsEditingProvider } from './types'; +import { CodeActionProviderService } from './codeActionProvider/main'; -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton<ISortImportsEditingProvider>(ISortImportsEditingProvider, SortImportsEditingProvider); +export function registerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + CodeActionProviderService, + ); } diff --git a/src/client/providers/signatureProvider.ts b/src/client/providers/signatureProvider.ts deleted file mode 100644 index f6ea0d65fd6e..000000000000 --- a/src/client/providers/signatureProvider.ts +++ /dev/null @@ -1,133 +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 { SIGNATURE } 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 = <SignatureInformation>{ - 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 <ParameterInformation>{ - documentation: arg.docstring.length > 0 ? arg.docstring : arg.description, - label: arg.name.trim() - }; - }); - } - signature.signatures.push(sig); - }); - return signature; - } - - return new SignatureHelp(); - } - @captureTelemetry(SIGNATURE) - public provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken): Thenable<SignatureHelp> { - // 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<proxy.IArgumentsResult> = { - command: proxy.CommandType.Arguments, - fileName: document.fileName, - columnIndex: position.character, - lineIndex: position.line, - source: document.getText() - }; - return this.jediFactory.getJediProxyHandler<proxy.IArgumentsResult>(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 28260841a49e..000000000000 --- a/src/client/providers/simpleRefactorProvider.ts +++ /dev/null @@ -1,167 +0,0 @@ -import * as vscode from 'vscode'; -import { getTextEditsFromPatch } from '../common/editor'; -import { IConfigurationService, 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 { REFACTOR_EXTRACT_FUNCTION, REFACTOR_EXTRACT_VAR } 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>(IInstaller); - let disposable = vscode.commands.registerCommand('python.refactorExtractVariable', () => { - const stopWatch = new StopWatch(); - const promise = extractVariable(context.extensionPath, - vscode.window.activeTextEditor!, - vscode.window.activeTextEditor!.selection, - // tslint:disable-next-line:no-empty - outputChannel, serviceContainer).catch(() => { }); - sendTelemetryWhenDone(REFACTOR_EXTRACT_VAR, promise, stopWatch); - }); - context.subscriptions.push(disposable); - - disposable = vscode.commands.registerCommand('python.refactorExtractMethod', () => { - const stopWatch = new StopWatch(); - const promise = extractMethod(context.extensionPath, - vscode.window.activeTextEditor!, - vscode.window.activeTextEditor!.selection, - // tslint:disable-next-line:no-empty - outputChannel, serviceContainer).catch(() => { }); - sendTelemetryWhenDone(REFACTOR_EXTRACT_FUNCTION, promise, stopWatch); - }); - context.subscriptions.push(disposable); -} - -// Exported for unit testing -export function extractVariable(extensionDir: string, textEditor: vscode.TextEditor, range: vscode.Range, - // tslint:disable-next-line:no-any - outputChannel: vscode.OutputChannel, serviceContainer: IServiceContainer): Promise<any> { - - 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; - const pythonSettings = serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(workspaceFolder ? workspaceFolder.uri : undefined); - - return validateDocumentForRefactor(textEditor).then(() => { - const newName = `newvariable${new Date().getMilliseconds().toString()}`; - const proxy = new RefactorProxy(extensionDir, pythonSettings, workspaceRoot, serviceContainer); - const rename = proxy.extractVariable<RenameResponse>(textEditor.document, newName, textEditor.document.uri.fsPath, range, textEditor.options).then(response => { - return response.results[0].diff; - }); - - return extractName(extensionDir, textEditor, range, newName, rename, outputChannel); - }); -} - -// Exported for unit testing -export function extractMethod(extensionDir: string, textEditor: vscode.TextEditor, range: vscode.Range, - // tslint:disable-next-line:no-any - outputChannel: vscode.OutputChannel, serviceContainer: IServiceContainer): Promise<any> { - - 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; - const pythonSettings = serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(workspaceFolder ? workspaceFolder.uri : undefined); - - return validateDocumentForRefactor(textEditor).then(() => { - const newName = `newmethod${new Date().getMilliseconds().toString()}`; - const proxy = new RefactorProxy(extensionDir, pythonSettings, workspaceRoot, serviceContainer); - const rename = proxy.extractMethod<RenameResponse>(textEditor.document, newName, textEditor.document.uri.fsPath, range, textEditor.options).then(response => { - return response.results[0].diff; - }); - - return extractName(extensionDir, textEditor, range, newName, rename, outputChannel); - }); -} - -// tslint:disable-next-line:no-any -function validateDocumentForRefactor(textEditor: vscode.TextEditor): Promise<any> { - if (!textEditor.document.isDirty) { - return Promise.resolve(); - } - - // tslint:disable-next-line:no-any - return new Promise<any>((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(extensionDir: string, textEditor: vscode.TextEditor, range: vscode.Range, newName: string, - // tslint:disable-next-line:no-any - renameResponse: Promise<string>, outputChannel: vscode.OutputChannel): Promise<any> { - 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 => console.error('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 89c724644fd8..000000000000 --- a/src/client/providers/symbolProvider.ts +++ /dev/null @@ -1,182 +0,0 @@ -'use strict'; - -import { - CancellationToken, DocumentSymbol, DocumentSymbolProvider, - Location, Range, SymbolInformation, SymbolKind, TextDocument, Uri -} from 'vscode'; -import { LanguageClient } from 'vscode-languageclient'; -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 { SYMBOL } 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. - SymbolKind[SymbolKind[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<SymbolInformation[]> { - const uri = document.uri; - const args = { textDocument: { uri: uri.toString() } }; - const raw = await this.languageClient.sendRequest<DocumentSymbol[]>( - '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<string, { timer: NodeJS.Timer; deferred: Deferred<SymbolInformation[]> }>; - private readonly fs: IFileSystem; - - public constructor(serviceContainer: IServiceContainer, private jediFactory: JediFactory, private readonly debounceTimeoutMs = 500) { - this.debounceRequest = new Map<string, { timer: NodeJS.Timer; deferred: Deferred<SymbolInformation[]> }>(); - this.fs = serviceContainer.get<IFileSystem>(IFileSystem); - } - - @captureTelemetry(SYMBOL) - public provideDocumentSymbols(document: TextDocument, token: CancellationToken): Thenable<SymbolInformation[]> { - return this.provideDocumentSymbolsThrottled(document, token); - } - - private provideDocumentSymbolsThrottled(document: TextDocument, token: CancellationToken): Thenable<SymbolInformation[]> { - const key = `${document.uri.fsPath}`; - if (this.debounceRequest.has(key)) { - const item = this.debounceRequest.get(key)!; - clearTimeout(item.timer); - item.deferred.resolve([]); - } - - const deferred = createDeferred<SymbolInformation[]>(); - const timer = setTimeout(() => { - if (token.isCancellationRequested) { - return deferred.resolve([]); - } - - const filename = document.fileName; - const cmd: proxy.ICommand<proxy.ISymbolResult> = { - command: proxy.CommandType.Symbols, - fileName: filename, - columnIndex: 0, - lineIndex: 0 - }; - - if (document.isDirty) { - cmd.source = document.getText(); - } - - this.jediFactory.getJediProxyHandler<proxy.ISymbolResult>(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<SymbolInformation[]> { - const filename = document.fileName; - - const cmd: proxy.ICommand<proxy.ISymbolResult> = { - command: proxy.CommandType.Symbols, - fileName: filename, - columnIndex: 0, - lineIndex: 0 - }; - - if (document.isDirty) { - cmd.source = document.getText(); - } - - return this.jediFactory.getJediProxyHandler<proxy.ISymbolResult>(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 87d60070e328..f68f151110ec 100644 --- a/src/client/providers/terminalProvider.ts +++ b/src/client/providers/terminalProvider.ts @@ -1,40 +1,72 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Disposable, Uri } from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { Disposable, Terminal } from 'vscode'; +import { IActiveResourceService, ICommandManager } from '../common/application/types'; import { Commands } from '../common/constants'; -import { ITerminalServiceFactory } from '../common/terminal/types'; +import { inTerminalEnvVarExperiment } from '../common/experiments/helpers'; +import { ITerminalActivator, ITerminalServiceFactory } from '../common/terminal/types'; +import { IConfigurationService, IExperimentService } from '../common/types'; +import { swallowExceptions } from '../common/utils/decorators'; import { IServiceContainer } from '../ioc/types'; -import { captureTelemetry } from '../telemetry'; -import { TERMINAL_CREATE } from '../telemetry/constants'; +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>(IActiveResourceService); } - public dispose() { - this.disposables.forEach(disposable => disposable.dispose()); + + @swallowExceptions('Failed to initialize terminal provider') + public async initialize(currentTerminal: Terminal | undefined): Promise<void> { + const configuration = this.serviceContainer.get<IConfigurationService>(IConfigurationService); + const experimentService = this.serviceContainer.get<IExperimentService>(IExperimentService); + const pythonSettings = configuration.getSettings(this.activeResourceService.getActiveResource()); + + if ( + currentTerminal && + pythonSettings.terminal.activateEnvInCurrentTerminal && + !inTerminalEnvVarExperiment(experimentService) && + !shouldEnvExtHandleActivation() + ) { + const hideFromUser = + 'hideFromUser' in currentTerminal.creationOptions && currentTerminal.creationOptions.hideFromUser; + if (!hideFromUser) { + const terminalActivator = this.serviceContainer.get<ITerminalActivator>(ITerminalActivator); + await terminalActivator.activateEnvironmentInTerminal(currentTerminal, { preserveFocus: true }); + } + sendTelemetryEvent(EventName.ACTIVATE_ENV_IN_CURRENT_TERMINAL, undefined, { + isTerminalVisible: !hideFromUser, + }); + } + } + + public dispose(): void { + this.disposables.forEach((disposable) => disposable.dispose()); } + private registerCommands() { const commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager); const disposable = commandManager.registerCommand(Commands.Create_Terminal, this.onCreateTerminal, this); this.disposables.push(disposable); } - @captureTelemetry(TERMINAL_CREATE, { triggeredBy: 'commandpalette' }) + + @captureTelemetry(EventName.TERMINAL_CREATE, { triggeredBy: 'commandpalette' }) private async onCreateTerminal() { + const activeResource = this.activeResourceService.getActiveResource(); + if (useEnvExtension()) { + const commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager); + await commandManager.executeCommand('python-envs.createTerminal', activeResource); + } + const terminalService = this.serviceContainer.get<ITerminalServiceFactory>(ITerminalServiceFactory); - const activeResource = this.getActiveResource(); await terminalService.createTerminalService(activeResource, 'Python').show(false); } - private getActiveResource(): Uri | undefined { - const documentManager = this.serviceContainer.get<IDocumentManager>(IDocumentManager); - if (documentManager.activeTextEditor && !documentManager.activeTextEditor.document.isUntitled) { - return documentManager.activeTextEditor.document.uri; - } - const workspace = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0 ? workspace.workspaceFolders[0].uri : undefined; - } } 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<WorkspaceEdit | undefined>; - sortImports(uri?: Uri): Promise<void>; - registerCommands(): void; -} diff --git a/src/client/providers/updateSparkLibraryProvider.ts b/src/client/providers/updateSparkLibraryProvider.ts deleted file mode 100644 index 935e5082a2e1..000000000000 --- a/src/client/providers/updateSparkLibraryProvider.ts +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { Commands } from '../common/constants'; -import { sendTelemetryEvent } from '../telemetry'; -import { UPDATE_PYSPARK_LIBRARY } from '../telemetry/constants'; - -export function activateUpdateSparkLibraryProvider(): vscode.Disposable { - return vscode.commands.registerCommand(Commands.Update_SparkLibrary, updateSparkLibrary); -} - -function updateSparkLibrary() { - const pythonConfig = vscode.workspace.getConfiguration('python'); - const extraLibPath = 'autoComplete.extraPaths'; - // tslint:disable-next-line:no-invalid-template-strings - const sparkHomePath = '${env:SPARK_HOME}'; - pythonConfig.update(extraLibPath, [path.join(sparkHomePath, 'python'), - path.join(sparkHomePath, 'python/pyspark')]).then(() => { - //Done - }, reason => { - vscode.window.showErrorMessage(`Failed to update ${extraLibPath}. Error: ${reason.message}`); - console.error(reason); - }); - vscode.window.showInformationMessage('Make sure you have SPARK_HOME environment variable set to the root path of the local spark installation!'); - sendTelemetryEvent(UPDATE_PYSPARK_LIBRARY); -} 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<void>; + stop(client: BaseLanguageClient): Promise<void>; + 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<IDiscoveryAPI>; + +/** + * 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<void> { + this.locator = await this.getLocator(); + } + + public get onProgress(): Event<ProgressNotificationEvent> { + 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<IDiscoveryAPI> { + 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 <Version> <bitness> (<env name>: <env type>)` + * 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<PythonEnvInfo> | 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<InterpreterInformation | undefined>; + /** + * 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<InterpreterInformation | undefined> { + 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<InterpreterInformation | undefined> { + 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<string, Deferred<InterpreterInformation>> = new Map< + string, + Deferred<InterpreterInformation> + >(); + + private workerPool?: IWorkerPool<PythonEnvInfo, InterpreterInformation | undefined>; + + private condaRunWorkerPool?: IWorkerPool<PythonEnvInfo, InterpreterInformation | undefined>; + + 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<InterpreterInformation | undefined> { + 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<InterpreterInformation>(); + 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<InterpreterInformation | undefined> { + 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<PythonEnvInfo, InterpreterInformation | undefined>( + 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<PythonEnvInfo, InterpreterInformation | undefined>, + 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<InterpreterInformation | undefined> { + 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<PythonVersion>(`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<I = PythonEnvInfo> = { + /** + * 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<I = PythonEnvInfo> extends IAsyncIterableIterator<I> { + /** + * 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<PythonEnvUpdatedEvent<I> | ProgressNotificationEvent>; +} + +export enum ProgressReportStage { + idle = 'idle', + discoveryStarted = 'discoveryStarted', + allPathsDiscovered = 'allPathsDiscovered', + discoveryFinished = 'discoveryFinished', +} + +export type ProgressNotificationEvent = { + stage: ProgressReportStage; +}; + +export function isProgressEvent<I = PythonEnvInfo>( + event: PythonEnvUpdatedEvent<I> | ProgressNotificationEvent, +): event is ProgressNotificationEvent { + return 'stage' in event; +} + +/** + * An empty Python envs iterator. + */ +export const NOOP_ITERATOR: IPythonEnvsIterator = iterEmpty<PythonEnvInfo>(); + +/** + * 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> = 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<I = PythonEnvInfo, E = PythonEnvsChangedEvent> extends IPythonEnvsWatcher<E> { + 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<E>): IPythonEnvsIterator<I>; +} + +export type ICompositeLocator<I = PythonEnvInfo, E = PythonEnvsChangedEvent> = Omit<ILocator<I, E>, '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<PythonEnvInfo | undefined>; +} + +export interface IResolvingLocator<I = PythonEnvInfo> extends IResolver, ICompositeLocator<I> {} + +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<ProgressNotificationEvent>; + /** + * Fires with details if the known list changes. + */ + readonly onChanged: Event<PythonEnvCollectionChangedEvent>; + /** + * 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<void> | undefined; + /** + * Triggers a new refresh for query if there isn't any already running. + */ + triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise<void>; + /** + * 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<PythonEnvInfo | undefined>; +} + +export interface IEmitter<E> { + 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<I = PythonEnvInfo, E = PythonEnvsChangedEvent> implements ILocator<I, E> { + public readonly onChanged: Event<E>; + + public abstract readonly providerId: string; + + protected readonly emitter: IEmitter<E>; + + constructor(watcher: IPythonEnvsWatcher<E> & IEmitter<E>) { + this.emitter = watcher; + this.onChanged = watcher.onChanged; + } + + // eslint-disable-next-line class-methods-use-this + public abstract iterEnvs(query?: QueryForEvent<E>): IPythonEnvsIterator<I>; +} + +/** + * 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<I = PythonEnvInfo> extends LocatorBase<I> { + 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<I = PythonEnvInfo>(iterator: IPythonEnvsIterator<I>): Promise<I[]> { + const envs: (I | undefined)[] = []; + + const updatesDone = createDeferred<void>(); + if (iterator.onUpdated === undefined) { + updatesDone.resolve(); + } else { + const listener = iterator.onUpdated((event: PythonEnvUpdatedEvent<I> | 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<I>(iterators: IPythonEnvsIterator<I>[]): IPythonEnvsIterator<I> { + const result: IPythonEnvsIterator<I> = 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<I> | ProgressNotificationEvent) => any) => { + const disposables = new Disposables(); + let numActive = events.length; + events.forEach((event) => { + const disposable = event!((e: PythonEnvUpdatedEvent<I> | 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<I = PythonEnvInfo> extends PythonEnvsWatchers implements ICompositeLocator<I> { + public readonly providerId: string; + + constructor( + // The locators will be watched as well as iterated. + private readonly locators: ReadonlyArray<ILocator<I>>, + ) { + super(locators); + this.providerId = locators.map((loc) => loc.providerId).join('+'); + } + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator<I> { + 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<NativeEnvInfo | NativeEnvManagerInfo>; + /** + * Will spawn the provided Python executable and return information about the environment. + * @param executable + */ + resolve(executable: string): Promise<NativeEnvInfo>; + /** + * Used only for telemetry. + */ + getCondaInfo(): Promise<NativeCondaInfo>; +} + +interface NativeLog { + level: string; + message: string; +} + +class NativePythonFinderImpl extends DisposableBase implements NativePythonFinder { + private readonly connection: rpc.MessageConnection; + + private firstRefreshResults: undefined | (() => AsyncGenerator<NativeEnvInfo, void, unknown>); + + private readonly outputChannel = this._register(createLogOutputChannel('Python Locator', { log: true })); + + private initialRefreshMetrics = { + timeToSpawn: 0, + timeToConfigure: 0, + timeToRefresh: 0, + }; + + private readonly suppressErrorNotification: IPersistentStorage<boolean>; + + constructor(private readonly cacheDirectory?: Uri, private readonly context?: IExtensionContext) { + super(); + this.suppressErrorNotification = this.context + ? getGlobalStorage<boolean>(this.context, DONT_SHOW_SPAWN_ERROR_AGAIN, false) + : ({ get: () => false, set: async () => {} } as IPersistentStorage<boolean>); + this.connection = this.start(); + void this.configure(); + this.firstRefreshResults = this.refreshFirstTime(); + } + + public async resolve(executable: string): Promise<NativeEnvInfo> { + await this.configure(); + const environment = await this.connection.sendRequest<NativeEnvInfo>('resolve', { + executable, + }); + + this.outputChannel.info(`Resolved Python Environment ${environment.executable}`); + return environment; + } + + async *refresh(options?: NativePythonEnvironmentKind | Uri[]): AsyncIterable<NativeEnvInfo> { + 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<void>; discovered: Event<NativeEnvInfo | NativeEnvManagerInfo> } { + const disposable = this._register(new DisposableStore()); + const discovered = disposable.add(new EventEmitter<NativeEnvInfo | NativeEnvManagerInfo>()); + const completed = createDeferred<void>(); + const pendingPromises: Promise<void>[] = []; + 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<void>) => { + 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<NativeEnvInfo>('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<string>(CONDAPATH_SETTING_KEY), + poetryExecutable: getPythonSettingAndUntildify<string>('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<NativeCondaInfo> { + return this.connection.sendRequest<NativeCondaInfo>('condaInfo'); + } + + private async handleSpawnError(errorMessage: string): Promise<void> { + // 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<string>(VENVPATH_SETTING_KEY); + if (venvPath) { + venvDirs.push(untildify(venvPath)); + } + const venvFolders = getPythonSettingAndUntildify<string[]>(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<T>(name: string, scope?: Uri): T | undefined { + const value = getConfiguration('python', scope).get<T>(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<void> { + 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, PythonEnvKind>([ + [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<PythonWorkspaceEnvEvent>; + + watchPath(uri: Uri, pattern?: string): void; + unwatchPath(uri: Uri): void; + onDidGlobalEnvChanged: Event<PythonGlobalEnvEvent>; +} + +/* + * 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<PythonWorkspaceEnvEvent>(); + + private readonly _onDidGlobalEnvChanged = new EventEmitter<PythonGlobalEnvEvent>(); + + private readonly _disposeMap: Map<string, Disposable> = new Map<string, Disposable>(); + + constructor() { + this.disposables.push(this._onDidWorkspaceEnvChanged, this._onDidGlobalEnvChanged); + } + + onDidGlobalEnvChanged: Event<PythonGlobalEnvEvent> = this._onDidGlobalEnvChanged.event; + + onDidWorkspaceEnvChanged: Event<PythonWorkspaceEnvEvent> = 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<BasicEnvInfo> 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<void>; + + private watchersReady?: Deferred<void>; + + /** + * This can be used to initialize resources when subclasses are created. + */ + protected async activate(): Promise<void> { + 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<void> { + await this.disposables.dispose(); + } + + public async *iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator<BasicEnvInfo> { + 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<BasicEnvInfo>; + + /** + * 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<void> { + // 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<void> { + // No watchers! + } + + protected async ensureResourcesReady(): Promise<void> { + if (this.resourcesReady !== undefined) { + await this.resourcesReady.promise; + return; + } + this.resourcesReady = createDeferred<void>(); + await this.initResources().catch((ex) => { + traceError(ex); + this.resourcesReady?.reject(ex); + }); + this.resourcesReady.resolve(); + } + + private async ensureWatchersReady(): Promise<void> { + if (this.watchersReady !== undefined) { + await this.watchersReady.promise; + return; + } + this.watchersReady = createDeferred<void>(); + + // 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<BasicPythonEnvCollectionChangedEvent>; + + /** + * 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<PythonEnvInfo | undefined>; + + /** + * 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<void>; + + /** + * 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<void>; +} + +interface IPersistentStorage { + get(): PythonEnvInfo[]; + store(envs: PythonEnvInfo[]): Promise<void>; +} + +/** + * Environment info cache using persistent storage to save and retrieve pre-cached env info. + */ +export class PythonEnvInfoCache extends PythonEnvsWatcher<PythonEnvCollectionChangedEvent> + implements IEnvsCollectionCache { + private envs: PythonEnvInfo[] = []; + + /** + * Carries the list of envs which have been validated to have latest info. + */ + private validatedEnvs = new Set<string>(); + + /** + * 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<string>(); + + constructor(private readonly persistentStorage: IPersistentStorage) { + super(); + } + + public async validateCache(envs?: PythonEnvInfo[], isCompleteList?: boolean): Promise<void> { + /** + * 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<PythonEnvInfo | undefined> { + // `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<void> { + 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<PythonEnvInfoCache> { + 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<PythonEnvCollectionChangedEvent> implements IDiscoveryAPI { + /** Keeps track of ongoing refreshes for various queries. */ + private refreshesPerQuery = new Map<PythonLocatorQuery | undefined, Deferred<void>>(); + + /** Keeps track of scheduled refreshes other than the ongoing one for various queries. */ + private scheduledRefreshesPerQuery = new Map<PythonLocatorQuery | undefined, Promise<void>>(); + + /** Keeps track of promises which resolves when a stage has been reached */ + private progressPromises = new Map<ProgressReportStage, Deferred<void>>(); + + /** Keeps track of whether a refresh has been triggered for various queries. */ + private hasRefreshFinishedForQuery = new Map<PythonLocatorQuery | undefined, boolean>(); + + private readonly progress = new EventEmitter<ProgressNotificationEvent>(); + + public refreshState = ProgressReportStage.discoveryFinished; + + public get onProgress(): Event<ProgressNotificationEvent> { + return this.progress.event; + } + + public getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | 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<PythonEnvInfo | undefined> { + 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<void> { + 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<void> { + 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<void>(); + 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<void> { + const refreshPromise = this.getRefreshPromiseForQuery(query); + let nextRefreshPromise: Promise<void>; + 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<void>()); + Object.values(ProgressReportStage).forEach((stage) => { + this.progressPromises.set(stage, createDeferred<void>()); + }); + 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<BasicEnvInfo> { + public get onChanged(): Event<PythonEnvsChangedEvent> { + return this.parentLocator.onChanged; + } + + constructor(private readonly parentLocator: ILocator<BasicEnvInfo>) {} + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator<BasicEnvInfo> { + const didUpdate = new EventEmitter<PythonEnvUpdatedEvent<BasicEnvInfo> | ProgressNotificationEvent>(); + const incomingIterator = this.parentLocator.iterEnvs(query); + const iterator = iterEnvsIterator(incomingIterator, didUpdate); + iterator.onUpdated = didUpdate.event; + return iterator; + } +} + +async function* iterEnvsIterator( + iterator: IPythonEnvsIterator<BasicEnvInfo>, + didUpdate: EventEmitter<PythonEnvUpdatedEvent<BasicEnvInfo> | ProgressNotificationEvent>, +): IPythonEnvsIterator<BasicEnvInfo> { + 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<PythonEnvUpdatedEvent<BasicEnvInfo> | 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<PythonEnvUpdatedEvent<BasicEnvInfo> | 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<PythonEnvsChangedEvent> { + return this.parentLocator.onChanged; + } + + constructor( + private readonly parentLocator: ICompositeLocator<BasicEnvInfo>, + 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<PythonEnvInfo | undefined> { + 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<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const incomingIterator = this.parentLocator.iterEnvs(query); + const iterator = this.iterEnvsIterator(incomingIterator, didUpdate); + iterator.onUpdated = didUpdate.event; + return iterator; + } + + private async *iterEnvsIterator( + iterator: IPythonEnvsIterator<BasicEnvInfo>, + didUpdate: EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>, + ): IPythonEnvsIterator { + const environmentKinds = new Map<string, PythonEnvKind>(); + 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<PythonEnvUpdatedEvent | ProgressNotificationEvent>, + 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<string, PythonEnvKind>) { + 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<PythonEnvUpdatedEvent | ProgressNotificationEvent>, +) { + 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<PythonEnvKind, (env: BasicEnvInfo) => Promise<PythonEnvInfo>> { + const resolvers = new Map<PythonEnvKind, (_: BasicEnvInfo) => Promise<PythonEnvInfo>>(); + 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<PythonEnvInfo> { + 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<void> { + // 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<PythonEnvInfo> { + 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<PythonEnvInfo> { + 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<PythonEnvInfo> { + 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<PythonEnvInfo> { + 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 <distro>` 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 <distro> <env-name>`. These are similar to environments + // created using `python -m venv <env-name>`. + // + // 3. Conda Envs : These are environments that are created when you use + // `pyenv virtualenv <miniconda|anaconda> <env-name>`. These are similar to + // environments created using `conda create -n <env-name>. + // + // All these environments are fully handled by `pyenv` and should be activated using + // `pyenv local|global <env-name>` or `pyenv shell <env-name>` + // + // 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<PythonEnvInfo> { + 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<PythonEnvInfo> { + 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<BasicEnvInfo> { + 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<BasicEnvInfo> { + 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<string[]> { + const venvDirs: string[] = []; + const venvPath = getPythonSetting<string>(VENVPATH_SETTING_KEY); + if (venvPath) { + venvDirs.push(untildify(venvPath)); + } + const venvFolders = getPythonSetting<string[]>(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<PythonEnvKind> { + 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<void> { + 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<BasicEnvInfo> { + 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<void> { + this.disposables.push( + onDidChangePythonSetting(DEFAULT_INTERPRETER_PATH_SETTING_KEY, () => this.fire(), this.root), + ); + } + + // eslint-disable-next-line class-methods-use-this + protected doIterEnvs(): IPythonEnvsIterator<BasicEnvInfo> { + const iterator = async function* (root: string) { + traceVerbose('Searching for custom workspace envs'); + const filename = getPythonSetting<string>(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<string>; + +/** + * A naive locator the wraps a function that finds Python executables. + */ +abstract class FoundFilesLocator implements ILocator<BasicEnvInfo> { + public abstract readonly providerId: string; + + public readonly onChanged: Event<PythonEnvsChangedEvent>; + + 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<BasicEnvInfo> { + const executables = this.getExecutables(); + async function* generator(kind: PythonEnvKind, source?: PythonEnvSource[]): IPythonEnvsIterator<BasicEnvInfo> { + for await (const executablePath of executables) { + yield { executablePath, kind, source }; + } + } + const iterator = generator(this.kind, this.source); + return iterator; + } +} + +type GetDirExecutablesFunc = (dir: string) => AsyncIterableIterator<string>; + +/** + * 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<string> { + 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 | string[], + /** + * Returns the kind of environment specific to locator given the path to executable. + */ + private readonly getKind: (executable: string) => Promise<PythonEnvKind>, + private readonly creationOptions: LocationWatchOptions | FileWatchOptions = {}, + private readonly watcherKind: FSWatcherKind = FSWatcherKind.Global, + ) { + super(); + this.activate().ignoreErrors(); + } + + protected async initWatchers(): Promise<void> { + // 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<string[]> { + 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<Uri | undefined> { + 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<PythonEnvKind> { + 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<BasicEnvInfo> { + // 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<string[]> { + 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<BasicEnvInfo> { + 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<string[]> { + 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<BasicEnvInfo> { + 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<string[]> { + 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<BasicEnvInfo> { + 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<string[]> { + 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<PythonEnvKind> { + 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<BasicEnvInfo> { + 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<BasicEnvInfo> { + public readonly providerId = 'posixKnownPaths'; + + private kind: PythonEnvKind = PythonEnvKind.OtherGlobal; + + public iterEnvs(): IPythonEnvsIterator<BasicEnvInfo> { + // 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 <pyenv dir>/versions directory and gets + * all the environments (global or virtual) in that directory. + */ +async function* getPyenvEnvironments(): AsyncIterableIterator<BasicEnvInfo> { + 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<BasicEnvInfo> { + 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<BasicEnvInfo>, IDisposable { + public readonly providerId: string = 'windows-path-env-var-locator'; + + public readonly onChanged: Event<PythonEnvsChangedEvent>; + + private readonly locators: Locators<BasicEnvInfo>; + + private readonly disposables = new Disposables(); + + constructor() { + const inExp = inExperiment(DiscoveryUsingWorkers.experiment); + const dirLocators: (ILocator<BasicEnvInfo> & 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<void> { + this.locators.dispose(); + await this.disposables.dispose(); + } + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator<BasicEnvInfo> { + // 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<BasicEnvInfo>) { + 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<string> { + for await (const entry of iterPythonExecutablesInDir(dirname)) { + if (await looksLikeBasicGlobalPython(entry)) { + yield entry.filename; + } + } +} + +async function* getExecutables(dirname: string): AsyncIterableIterator<string> { + 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<BasicEnvInfo> & 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<BasicEnvInfo> { + 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<BasicEnvInfo> { + 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<BasicEnvInfo> { + 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<PythonEnvsChangedEvent>): IPythonEnvsIterator<BasicEnvInfo> { + loadAllEnvs(changed).ignoreErrors(); +} + +async function loadAllEnvs(changed: IEmitter<PythonEnvsChangedEvent>) { + 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<BasicEnvInfo> { + 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<string[]> { + 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<PythonEnvKind> { + 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<BasicEnvInfo> { + 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<I = PythonEnvInfo> extends Locators<I> { + constructor( + // These are expected to be low-level locators (e.g. system). + private readonly nonWorkspace: ILocator<I>[], + // This is expected to be a locator wrapping any found in + // the workspace (i.e. WorkspaceLocators). + private readonly workspace: ILocator<I>, + ) { + super([...nonWorkspace, workspace]); + } + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator<I> { + const iterators: IPythonEnvsIterator<I>[] = [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<BasicEnvInfo> & Partial<IDisposable>; +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<RootURI, [ILocator<BasicEnvInfo>, IDisposable]> = {}; + + private readonly roots: Record<RootURI, Uri> = {}; + + constructor(private readonly watchRoots: WatchRootsFunc, private readonly factories: WorkspaceLocatorFactory[]) { + super(); + this.activate().ignoreErrors(); + } + + public async dispose(): Promise<void> { + 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<BasicEnvInfo> { + 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<BasicEnvInfo>(); + } + if (query.providerId && query.providerId !== this.providerId) { + // This is a request for a specific provider, so skip it. + return iterEmpty<BasicEnvInfo>(); + } + } + // The query matches or was not location-specific. + const [locator] = this.locators[key]; + return locator.iterEnvs(query); + }); + return combineIterators(iterators); + } + + protected async initResources(): Promise<void> { + 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<BasicEnvInfo>[] = []; + 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<E = PythonEnvsChangedEvent> { + /** + * The hook for registering event listeners (callbacks). + */ + readonly onChanged: Event<E>; +} + +/** + * 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<T = PythonEnvsChangedEvent> implements IPythonEnvsWatcher<T> { + /** + * The hook for registering event listeners (callbacks). + */ + public readonly onChanged: Event<T>; + + private readonly didChange = new EventEmitter<T>(); + + 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<PythonEnvsChangedEvent>; + + private readonly watcher = new PythonEnvsWatcher(); + + private readonly disposables = new Disposables(); + + constructor(watchers: ReadonlyArray<IPythonEnvsWatcher>) { + 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<void> { + 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<boolean> { + 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<DirEntry> { + // "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<DirEntry> { + 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<DirEntry> { + 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<DirEntry[]> { + 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<PythonVersion> { + 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<PythonVersion> { + 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<boolean>; + }, +): Promise<boolean> { + 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<boolean> { + // "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<boolean> { + // "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<string | undefined> { + 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<PythonEnvKind, (path: string) => Promise<boolean>> { + const defaultTrue = () => Promise.resolve(true); + const identifier: Map<PythonEnvKind, (path: string) => Promise<boolean>> = 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<PythonEnvKind> { + 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<boolean> { + const execDir = path.dirname(interpreterPath); + const runtimeDir = path.dirname(execDir); + return pathExists(path.join(runtimeDir, '_runtime_store')); +} + +export class ActiveState { + private static statePromise: Promise<ActiveState | undefined> | undefined; + + public static async getState(): Promise<ActiveState | undefined> { + 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<ActiveState | undefined> { + const stateToolDir = this.getStateToolDir(); + const stateCommand = + getPythonSetting<string>(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand; + if (stateToolDir && ((await pathExists(stateToolDir)) || stateCommand !== this.defaultStateCommand)) { + return new ActiveState(); + } + return undefined; + } + + public async getProjects(): Promise<ProjectInfo[] | undefined> { + 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<ProjectInfo[] | undefined> { + try { + const stateCommand = + getPythonSetting<string>(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<boolean>, + getPythonInfo: (python: string) => Promise<Partial<PythonEnvironment> | 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<boolean> { + 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<string[]> { + 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<PythonVersion> { + 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<string | undefined, Promise<Conda | undefined>>(); + + private condaInfoCached = new Map<string | undefined, Promise<CondaInfo> | 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<string | undefined, Promise<Conda | undefined>>(); + }); + } + + public static async getConda(shellPath?: string): Promise<Conda | undefined> { + 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<Conda | undefined> { + traceVerbose(`Searching for conda.`); + const home = getUserHomeDir(); + let customCondaPath: string | undefined = 'conda'; + try { + customCondaPath = getPythonSetting<string>(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 "<prefix>/*conda*/<suffix>", 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<CondaInfo> { + 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<CondaInfo> { + 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<CondaEnvInfo[]> { + 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<string[]> { + const info = await this.getInfo(); + return info.envs_dirs ?? []; + } + + public async getName(prefix: string, info?: CondaInfo): Promise<string | undefined> { + 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<CondaEnvInfo | undefined> { + 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<string> { + 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<string[] | undefined> { + 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<string[] | undefined> { + 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<SemVer | undefined> { + const info = await this.getInfo(true).catch<CondaInfo | undefined>(() => 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<string | undefined>(() => undefined); + + versionString = stdOut && stdOut.startsWith('conda ') ? stdOut.substring('conda '.length).trim() : stdOut; + } + if (!versionString) { + return undefined; + } + const pattern = /(?<major>\d+)\.(?<minor>\d+)\.(?<micro>\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<boolean> { + 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<string[] | undefined> { + const conda = await Conda.getConda(); + return conda?.getEnvDirs(); +} + +export function getCondaPathSetting(): string | undefined { + const config = getConfiguration('python'); + return config.get<string>(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<string> { + 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<string | undefined> { + 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<boolean> { + 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<SemVer | undefined> { + 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<string | undefined> { + 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<CondaInfo | undefined> { + 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<boolean> { + // 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<boolean> { + 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<string, Promise<Hatch | undefined>> = new Map< + string, + Promise<Hatch | undefined> + >(); + + /** + * 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<Hatch | undefined> { + 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<Hatch | undefined> { + // 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<string[] | undefined> { + 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<string[] | undefined> { + 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<boolean> { + 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<boolean> { + 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<string | undefined> { + 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<string | undefined> { + // 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<string | undefined> { + // 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 + // <Environment folder> + // |__ .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<string | undefined> { + 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<boolean> { + 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<boolean> { + 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<boolean> { + 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<readonly string[]> { + 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<string[] | undefined> { + 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<PixiInfo | undefined> { + 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<PixiEnvMetadata | undefined> { + 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<Pixi | undefined> { + let pixi = getPythonSetting<string>(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<Pixi | undefined> | 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<Pixi | undefined> { + 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<PixiEnvironmentInfo | undefined> { + 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 `<projectDir>/.pixi/envs/<environment>/`. 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<string[] | undefined> { + 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<string[] | undefined> { + 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: + * + * <sanitized_project_name>-<project_cwd_hash>-py<major>.<micro> + * + * Implementation details behind <sanitized_project_name> and <project_cwd_hash> 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<boolean> { + 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<boolean> { + // 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<boolean> { + 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<string, Promise<Poetry | undefined>> = new Map< + string, + Promise<Poetry | undefined> + >(); + + /** + * 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<Poetry | undefined> { + // 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<Poetry | undefined> { + // 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<string>('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<string[] | undefined> { + 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<string[] | undefined> { + const result = await this.safeShellExecute(`${this.command} env list --full-path`); + if (!result) { + return undefined; + } + /** + * We expect stdout to contain something like: + * + * <full-path>\poetry_2-tutorial-project-6hnqYwvD-py3.7 + * <full-path>\poetry_2-tutorial-project-6hnqYwvD-py3.8 + * <full-path>\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<string | undefined> { + 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<string | undefined> { + 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<string | undefined> { + 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<boolean> { + 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<boolean> { + 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<string> { + 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<string | undefined> { + 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<boolean> { + 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<string, (path: string) => 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<string, (path: string) => 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<boolean> { + 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<boolean> { + 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<boolean> { + // 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<string> { + 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<string> { + // 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<boolean> { + 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<PythonVersion> { + 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<ExecutionResult<string>> { + const useWorker = false; + const service = await internalServiceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(); + options = { ...options, useWorker }; + return service.shellExec(command, options); +} + +export async function exec( + file: string, + args: string[], + options: SpawnOptions = {}, + useWorker = false, +): Promise<ExecutionResult<string>> { + const service = await internalServiceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create(); + options = { ...options, useWorker }; + return service.exec(file, args, options); +} + +export function inExperiment(experimentName: string): boolean { + const service = internalServiceContainer.get<IExperimentService>(IExperimentService); + return service.inExperimentSync(experimentName); +} + +// Workspace + +export function isVirtualWorkspace(): boolean { + const service = internalServiceContainer.get<IWorkspaceService>(IWorkspaceService); + return service.isVirtualWorkspace; +} + +// filesystem + +export function pathExists(absPath: string): Promise<boolean> { + return fsapi.pathExists(absPath); +} + +export function pathExistsSync(absPath: string): boolean { + return fsapi.pathExistsSync(absPath); +} + +export function readFile(filePath: string): Promise<string> { + 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<boolean> { + 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<string> { + 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<boolean> { + 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<string> { + 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.<name>`. + * @param name The name of the setting. + */ +export function getPythonSetting<T>(name: string, root?: string): T | undefined { + const resource = root ? vscode.Uri.file(root) : undefined; + const settings = internalServiceContainer.get<IConfigurationService>(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<string[]> { + 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<string[]> { + const binToLinkMap = new Map<string, string[]>(); + 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<IRegistryValue[]> { + if (!useWorkerThreads) { + // eslint-disable-next-line global-require + const WinReg = require('winreg'); + const regKey = new WinReg(options); + const deferred = createDeferred<RegistryItem[]>(); + 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<IRegistryKey[]> { + if (!useWorkerThreads) { + // eslint-disable-next-line global-require + const WinReg = require('winreg'); + const regKey = new WinReg(options); + const deferred = createDeferred<Registry[]>(); + 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<IRegistryInterpreterData | undefined> { + 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<IRegistryInterpreterData[]> { + 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<IRegistryInterpreterData[]> | undefined; + +export async function getRegistryInterpreters(): Promise<IRegistryInterpreterData[]> { + if (!isTestExecution() && registryInterpretersPromise !== undefined) { + return registryInterpretersPromise; + } + registryInterpretersPromise = getRegistryInterpretersImpl(); + return registryInterpretersPromise; +} + +async function getRegistryInterpretersImpl(useWorkerThreads = false): Promise<IRegistryInterpreterData[]> { + 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<void> { + 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<boolean> { + 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<boolean> { + 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<boolean> { + // 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<boolean> { + 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<boolean> { + 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<boolean> { + const extension = getExtension<PythonExtension>(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<string>(CREATE_ENV_TRIGGER_SETTING_PART, 'off'); + return value !== 'off'; + } + + return getWorkspaceStateValue<string>(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<string>('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<Diagnostic[]> { + 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<boolean> { + 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<QuickPickItem[]> { + 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<WorkspaceFolder | WorkspaceFolder[] | undefined> { + 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<CreateEnvironmentResult | undefined> => { + if (useEnvExtension()) { + try { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: undefined, + pythonVersion: undefined, + }); + const result = await executeCommand<PythonEnvironment | undefined>( + '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<void> => { + 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<CreateEnvironmentResult | undefined> => { + 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<void> { + const config = getConfiguration('python'); + const showCreateEnvButton = config.get<string>('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<EnvironmentWillCreateEvent>(); +const onCreateEnvironmentExitedEvent = new EventEmitter<EnvironmentDidCreateEvent>(); + +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<EnvironmentWillCreateEvent>; + onCreateEnvironmentExited: Event<EnvironmentDidCreateEvent>; + isCreatingEnvironment: () => boolean; +} { + return { + onCreateEnvironmentStarted: onCreateEnvironmentStartedEvent.event, + onCreateEnvironmentExited: onCreateEnvironmentExitedEvent.event, + isCreatingEnvironment: isBusyCreatingEnvironment, + }; +} + +async function createEnvironment( + provider: CreateEnvironmentProvider, + options: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, +): Promise<CreateEnvironmentResult | undefined> { + 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<CreateEnvironmentProvider | undefined> { + 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<CreateEnvironmentResult | undefined> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<string, boolean> = 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<void> { + 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<CreateEnvironmentResult | undefined>; + + /** + * Unique ID for the creation provider, typically <ExtensionId>:<environment-type | guid> + */ + 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<EnvironmentWillCreateEvent>; + + /** + * 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<EnvironmentDidCreateEvent>; + + /** + * 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<CreateEnvironmentResult | undefined>; + + /** + * 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<string> { + progress.report({ + message: CreateEnv.Conda.creating, + }); + + const deferred = createDeferred<string>(); + 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<CreateEnvironmentResult | undefined> { + 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<CreateEnvironmentResult | undefined> => 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<boolean> { + 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<string | undefined> { + 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<string | undefined> { + 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<boolean> { + const condaEnvPath = getPrefixCondaEnvPath(workspaceFolder); + return withProgress<boolean>( + { + 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<ExistingCondaAction> { + 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<string | undefined> { + progress.report({ + message: CreateEnv.Venv.creating, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'venv', + pythonVersion: undefined, + }); + + const deferred = createDeferred<string | undefined>(); + 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<CreateEnvironmentResult | undefined> { + 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<CreateEnvironmentResult | undefined> => 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<boolean> { + 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<boolean> { + 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<boolean> { + 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<boolean> { + 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<string>(); + + private readonly reportActions = new Map<string, (progress: CreateEnvironmentProgress) => 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<void> { + let dispose: Disposable | undefined; + try { + const deferred = createDeferred<void>(); + 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<string[] | undefined> { + 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<string[] | undefined> { + 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<string[] | undefined> { + 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<QuickPickItem>) => { + 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<IPackageInstallSelection[] | undefined> { + 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<boolean> { + const venvPath = getVenvPath(workspaceFolder); + return withProgress<boolean>( + { + 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<ExistingVenvAction> { + 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<void> { + 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/exec.ts b/src/client/pythonEnvironments/exec.ts new file mode 100644 index 000000000000..bd07a2a6192c --- /dev/null +++ b/src/client/pythonEnvironments/exec.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * A representation of the information needed to run a Python executable. + * + * @prop command - the executable to execute in a new OS process + * @prop args - the full list of arguments with which to invoke the command + * @prop python - the command + the arguments needed just to invoke Python + * @prop pythonExecutable - the path the the Python executable + */ +export type PythonExecInfo = { + command: string; + args: string[]; + + python: string[]; + pythonExecutable: string; +}; + +/** + * Compose Python execution info for the given executable. + * + * @param python - the path (or command + arguments) to use to invoke Python + * @param pythonArgs - any extra arguments to use when running Python + */ +export function buildPythonExecInfo( + python: string | string[], + pythonArgs?: string[], + pythonExecutable?: string, +): PythonExecInfo { + if (Array.isArray(python)) { + const args = python.slice(1); + if (pythonArgs) { + args.push(...pythonArgs); + } + return { + args, + command: python[0], + python: [...python], + pythonExecutable: pythonExecutable ?? python[python.length - 1], + }; + } + return { + command: python, + args: pythonArgs || [], + python: [python], + pythonExecutable: python, + }; +} + +/** + * Create a copy, optionally adding to the args to pass to Python. + * + * @param orig - the object to copy + * @param extraPythonArgs - any arguments to add to the end of `orig.args` + */ +export function copyPythonExecInfo(orig: PythonExecInfo, extraPythonArgs?: string[]): PythonExecInfo { + const info = { + command: orig.command, + args: [...orig.args], + python: [...orig.python], + pythonExecutable: orig.pythonExecutable, + }; + if (extraPythonArgs) { + info.args.push(...extraPythonArgs); + } + if (info.pythonExecutable === undefined) { + 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<string>('locator', 'js') === 'native'; +} + +/** + * Set up the Python environments component (during extension activation).' + */ +export async function initialize(ext: ExtensionState): Promise<IDiscoveryAPI> { + // 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<ActivationResult> { + /** + * 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<PythonEnvInfo[]>(ext.context, PYTHON_ENV_INFO_CACHE_KEY, []).get().length > 0; + if (!wasTriggered) { + api.triggerRefresh().ignoreErrors(); + folders?.forEach(async (folder) => { + const wasTriggeredForFolder = getGlobalStorage<boolean>( + 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<boolean>( + 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<IDiscoveryAPI> { + // Create the low-level locators. + const locators: ILocator<BasicEnvInfo> = new ExtensionLocators<BasicEnvInfo>( + // 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<BasicEnvInfo>[] { + const locators: (ILocator<BasicEnvInfo> & Partial<IDisposable>)[] = []; + 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[]>): 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<PythonEnvInfo[]>, envs: PythonEnvInfo[]): Promise<void> { + 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<IEnvsCollectionCache> { + const storage = getGlobalStorage<PythonEnvInfo[]>(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 new file mode 100644 index 000000000000..70c74329c49b --- /dev/null +++ b/src/client/pythonEnvironments/info/executable.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { getExecutable } from '../../common/process/internal/python'; +import { ShellExecFunc } from '../../common/process/types'; +import { traceError } from '../../logging'; +import { copyPythonExecInfo, PythonExecInfo } from '../exec'; + +/** + * Find the filename for the corresponding Python executable. + * + * Effectively, we look up `sys.executable`. + * + * @param python - the information to use when running Python + * @param shellExec - the function to use to run Python + */ +export async function getExecutablePath(python: PythonExecInfo, shellExec: ShellExecFunc): Promise<string | undefined> { + 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 new file mode 100644 index 000000000000..08310767914a --- /dev/null +++ b/src/client/pythonEnvironments/info/index.ts @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Architecture } from '../../common/utils/platform'; +import { PythonEnvType } from '../base/info'; +import { PythonVersion } from './pythonVersion'; + +/** + * The supported Python environment types. + */ +export enum EnvironmentType { + Unknown = 'Unknown', + Conda = 'Conda', + VirtualEnv = 'VirtualEnv', + Pipenv = 'PipEnv', + Pyenv = 'Pyenv', + Venv = 'Venv', + 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]; + +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 IModuleInstaller implementations. + */ +export enum ModuleInstallerType { + Unknown = 'Unknown', + Conda = 'Conda', + Pip = 'Pip', + Poetry = 'Poetry', + Pipenv = 'Pipenv', + Pixi = 'Pixi', +} + +/** + * Details about a Python runtime. + * + * @prop path - the location of the executable file + * @prop version - the runtime version + * @prop sysVersion - the raw value of `sys.version` + * @prop architecture - of the host CPU (e.g. `x86`) + * @prop sysPrefix - the environment's install root (`sys.prefix`) + * @prop pipEnvWorkspaceFolder - the pipenv root, if applicable + */ +export type InterpreterInformation = { + path: string; + version?: PythonVersion; + sysVersion?: string; + architecture: Architecture; + sysPrefix: string; + pipEnvWorkspaceFolder?: string; +}; + +/** + * Details about a Python environment. + * + * @prop companyDisplayName - the user-facing name of the distro publisher + * @prop displayName - the user-facing name for the environment + * @prop 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 PythonEnvironment = InterpreterInformation & { + id?: string; + companyDisplayName?: string; + displayName?: string; + detailedDisplayName?: string; + envType: EnvironmentType; + envName?: string; + envPath?: string; + cachedEntry?: boolean; + type?: PythonEnvType; +}; + +/** + * Convert the Python environment type to a user-facing name. + */ +export function getEnvironmentTypeName(environmentType: EnvironmentType): string { + switch (environmentType) { + case EnvironmentType.Conda: { + return 'conda'; + } + case EnvironmentType.Pipenv: { + return 'Pipenv'; + } + case EnvironmentType.Pyenv: { + return 'pyenv'; + } + case EnvironmentType.Venv: { + return 'venv'; + } + case EnvironmentType.VirtualEnv: { + return 'virtualenv'; + } + 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 ''; + } + } +} diff --git a/src/client/pythonEnvironments/info/interpreter.ts b/src/client/pythonEnvironments/info/interpreter.ts new file mode 100644 index 000000000000..8fe9bc7d49a8 --- /dev/null +++ b/src/client/pythonEnvironments/info/interpreter.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { SemVer } from 'semver'; +import { InterpreterInformation } from '.'; +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'; + +/** + * 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 && ['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: new SemVer(rawVersion), + sysVersion: raw.sysVersion, + sysPrefix: raw.sysPrefix, + }; +} + +type Logger = { + verbose(msg: string): void; + error(msg: string): void; +}; + +/** + * Collect full interpreter information from the given Python executable. + * + * @param python - the information to use when running Python + * @param shellExec - the function to use to exec Python + * @param logger - if provided, used to log failures or other info + */ +export async function getInterpreterInfo( + python: PythonExecInfo, + shellExec: ShellExecFunc, + logger?: Logger, +): Promise<InterpreterInformation | undefined> { + 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}"` : `"${replaceAll(c, '\\', '\\\\')}"`), ''); + + // Try shell execing the command, followed by the arguments. This will make node kill the process if it + // takes too long. + // Sometimes the python path isn't valid, timeout if that's the case. + // See these two bugs: + // https://github.com/microsoft/vscode-python/issues/7569 + // https://github.com/microsoft/vscode-python/issues/7760 + const result = await shellExec(quoted, { timeout: 15000 }); + if (result.stderr) { + if (logger) { + logger.error(`Failed to parse interpreter information for ${argv} stderr: ${result.stderr}`); + } + } + const json = parse(result.stdout); + if (logger) { + 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 new file mode 100644 index 000000000000..d61fcf14db4d --- /dev/null +++ b/src/client/pythonEnvironments/info/pythonVersion.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * A representation of a Python runtime's version. + * + * @prop raw - the original version string + * @prop major - the "major" version + * @prop minor - the "minor" version + * @prop patch - the "patch" (or "micro") version + * @prop build - the build ID of the executable + * @prop prerelease - identifies a tag in the release process (e.g. beta 1) + */ +// Note that this is currently compatible with SemVer objects, +// but we may change it to match the format of sys.version_info. +export type PythonVersion = { + raw: string; + major: number; + minor: number; + patch: number; + // Eventually it may be useful to match what sys.version_info + // provides for the remainder here: + // * releaseLevel: 'alpha' | 'beta' | 'candidate' | 'final'; + // * serial: number; + build: string[]; + prerelease: string[]; +}; + +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 new file mode 100644 index 000000000000..49df2ee03f21 --- /dev/null +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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 { + 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'; + +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); +} + +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, + }; + + const envType = convertedKinds.get(kind); + if (envType !== undefined) { + env.envType = envType; + } + // Otherwise it stays Unknown. + + if (version !== undefined) { + const { release, sysVersion } = version; + if (release === undefined) { + env.sysVersion = ''; + } else { + env.sysVersion = sysVersion; + } + + const semverLikeVersion: PythonVersion = toSemverLikeVersion(version); + env.version = semverLikeVersion; + } + + 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 ComponentAdapter implements IComponentAdapter { + private readonly changed = new vscode.EventEmitter<PythonEnvironmentsChangedEvent>(); + + constructor( + // The adapter only wraps one thing: the component API. + private readonly api: IDiscoveryAPI, + ) { + 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 triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise<void> { + return this.api.triggerRefresh(query, options); + } + + public getRefreshPromise(options?: GetRefreshEnvironmentsOptions) { + return this.api.getRefreshPromise(options); + } + + public get onProgress() { + return this.api.onProgress; + } + + public get onChanged() { + return this.changed.event; + } + + // For use in VirtualEnvironmentPrompt.activate() + + // 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(); + } + }); + } + + // Implements IInterpreterHelper + public async getInterpreterInformation(pythonPath: string): Promise<Partial<PythonEnvironment> | undefined> { + const env = await this.api.resolveEnv(pythonPath); + return env ? convertEnvInfo(env) : undefined; + } + + // eslint-disable-next-line class-methods-use-this + public async isMacDefaultPythonPath(pythonPath: string): Promise<boolean> { + // 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); + } + + // Implements IInterpreterService + + // We use the same getInterpreters() here as for IInterpreterLocatorService. + public async getInterpreterDetails(pythonPath: string): Promise<PythonEnvironment | undefined> { + 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; + } + } + + // Implements ICondaService + + // eslint-disable-next-line class-methods-use-this + public async isCondaEnvironment(interpreterPath: string): Promise<boolean> { + // 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); + } + + public async getCondaEnvironment(interpreterPath: string): Promise<CondaEnvironmentInfo | undefined> { + if (!(await isCondaEnvironment(interpreterPath))) { + // Undefined is expected here when the env is not Conda env. + return undefined; + } + + // 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); + + if (!env) { + return undefined; + } + + return { name: env.name, path: env.location }; + } + + // eslint-disable-next-line class-methods-use-this + public async isMicrosoftStoreInterpreter(pythonPath: string): Promise<boolean> { + // 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); + } + + // Implements IInterpreterLocatorService + public async hasInterpreters( + filter: (e: PythonEnvironment) => Promise<boolean> = async () => true, + ): Promise<boolean> { + 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; + } + + 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); + } + + public async getWorkspaceVirtualEnvInterpreters( + resource: vscode.Uri, + options?: { ignoreCache?: boolean }, + ): Promise<PythonEnvironment[]> { + 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); + } +} + +export function registerNewDiscoveryForIOC(serviceManager: IServiceManager, api: IDiscoveryAPI): void { + serviceManager.addSingleton<ICondaService>(ICondaService, CondaService); + serviceManager.addSingletonInstance<IComponentAdapter>(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<ProgressNotificationEvent>; + + private _onChanged: EventEmitter<PythonEnvCollectionChangedEvent>; + + private _refreshPromise?: Deferred<void>; + + private _envs: PythonEnvInfo[] = []; + + private _disposables: Disposable[] = []; + + private _condaEnvDirs: string[] = []; + + constructor(private readonly finder: NativePythonFinder) { + this._onProgress = new EventEmitter<ProgressNotificationEvent>(); + this._onChanged = new EventEmitter<PythonEnvCollectionChangedEvent>(); + + 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<ProgressNotificationEvent>; + + onChanged: Event<PythonEnvCollectionChangedEvent>; + + getRefreshPromise(_options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined { + return this._refreshPromise?.promise; + } + + triggerRefresh(_query?: PythonLocatorQuery, _options?: TriggerRefreshOptions): Promise<void> { + 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<PythonEnvInfo | undefined> { + 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<void> { + 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<void> { + 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/contracts.ts b/src/client/refactor/contracts.ts deleted file mode 100644 index 272c0c25c2de..000000000000 --- a/src/client/refactor/contracts.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface ExtractResult { - diff: string; -} \ No newline at end of file diff --git a/src/client/refactor/proxy.ts b/src/client/refactor/proxy.ts deleted file mode 100644 index bcf143f38ba2..000000000000 --- a/src/client/refactor/proxy.ts +++ /dev/null @@ -1,183 +0,0 @@ -// tslint:disable:no-any no-empty member-ordering prefer-const prefer-template no-var-self - -import { ChildProcess } from 'child_process'; -import * as path from 'path'; -import { Disposable, Position, Range, TextDocument, TextEditorOptions, Uri, window } from 'vscode'; -import '../common/extensions'; -import { IS_WINDOWS } from '../common/platform/constants'; -import { IPythonExecutionFactory } from '../common/process/types'; -import { IPythonSettings } from '../common/types'; -import { createDeferred, Deferred } from '../common/utils/async'; -import { getWindowsLineEndingCount } from '../common/utils/text'; -import { IServiceContainer } from '../ioc/types'; - -export class RefactorProxy extends Disposable { - private _process?: ChildProcess; - private _extensionDir: string; - private _previousOutData: string = ''; - private _previousStdErrData: string = ''; - private _startedSuccessfully: boolean = false; - private _commandResolve?: (value?: any | PromiseLike<any>) => void; - private _commandReject!: (reason?: any) => void; - private initialized!: Deferred<void>; - constructor(extensionDir: string, private pythonSettings: IPythonSettings, private workspaceRoot: string, - private serviceContainer: IServiceContainer) { - super(() => { }); - this._extensionDir = extensionDir; - } - - 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<T>(document: TextDocument, name: string, filePath: string, range: Range, options?: TextEditorOptions): Promise<T> { - 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<T>(JSON.stringify(command)); - } - public extractVariable<T>(document: TextDocument, name: string, filePath: string, range: Range, options?: TextEditorOptions): Promise<T> { - 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<T>(JSON.stringify(command)); - } - public extractMethod<T>(document: TextDocument, name: string, filePath: string, range: Range, options?: TextEditorOptions): Promise<T> { - 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<T>('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<T>(JSON.stringify(command)); - } - private sendCommand<T>(command: string, telemetryEvent?: string): Promise<T> { - return this.initialize(this.pythonSettings.pythonPath).then(() => { - // tslint:disable-next-line:promise-must-complete - return new Promise<T>((resolve, reject) => { - this._commandResolve = resolve; - this._commandReject = reject; - this._process!.stdin.write(command + '\n'); - }); - }); - } - private async initialize(pythonPath: string): Promise<void> { - const pythonProc = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource: Uri.file(this.workspaceRoot) }); - this.initialized = createDeferred<void>(); - const args = ['refactor.py', this.workspaceRoot]; - const cwd = path.join(this._extensionDir, 'pythonFiles'); - const result = pythonProc.execObservable(args, { cwd }); - 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); - } 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) { - console.error(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) { - 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 = dataStr.split(/\r?\n/g).filter(line => line.length > 0).map(resp => JSON.parse(resp)); - 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<NativeRepl> { + 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<string | undefined>(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<void> { + // 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<void> { + 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<boolean> - True if complete/Valid code is present, False otherwise. + */ + public async checkUserInputCompleteCode(activeEditor: TextEditor | undefined): Promise<boolean> { + 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<void> { + let wsMementoUri: Uri | undefined; + + if (!this.notebookDocument) { + const wsMemento = getWorkspaceStateValue<string>(NATIVE_REPL_URI_MEMENTO); + wsMementoUri = wsMemento ? Uri.parse(wsMemento) : undefined; + + if (!wsMementoUri || getTabNameForUri(wsMementoUri) !== 'Python REPL') { + await updateWorkspaceStateValue<string | undefined>(NATIVE_REPL_URI_MEMENTO, undefined); + wsMementoUri = undefined; + } + } + + const result = await openInteractiveREPL(this.notebookDocument ?? wsMementoUri, preserveFocus); + if (result) { + this.notebookDocument = result.notebookEditor.notebook; + await updateWorkspaceStateValue<string | undefined>( + 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<NativeRepl> { + 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<void>; + readonly isExecuting: boolean; + readonly isDisposed: boolean; + execute(code: string): Promise<ExecutionResult | undefined>; + executeSilently(code: string): Promise<ExecutionResult | undefined>; + interrupt(): void; + input(): void; + checkValidCommand(code: string): Promise<boolean>; +} + +class PythonServerImpl implements PythonServer, Disposable { + private readonly disposables: Disposable[] = []; + + private readonly _onCodeExecuted = new EventEmitter<void>(); + + 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<ExecutionResult | undefined> { + const result = await this.executeCode(code); + if (result?.status) { + this._onCodeExecuted.fire(); + } + return result; + } + + public executeSilently(code: string): Promise<ExecutionResult | undefined> { + return this.executeCode(code); + } + + private async executeCode(code: string): Promise<ExecutionResult | undefined> { + 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<boolean> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<string | undefined> { + 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<boolean>('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<PythonEnvironment | undefined> { + 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<number | undefined> { + 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<IVariableDescription[]>; +} 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<IVariableDescription[]> { + 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<string> { + 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<string, VariablesResult[]>(); + + 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<NotebookDocument>(); + + onDidChangeVariables = this._onDidChangeVariables.event; + + private executionCount = 0; + + constructor( + private readonly variableRequester: VariableRequester, + private readonly getNotebookDocument: () => NotebookDocument | undefined, + codeExecutedEvent: Event<void>, + ) { + 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<VariablesResult> { + 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<IVariableDescription[]> { + 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 455ee72f8391..000000000000 --- a/src/client/sourceMapSupport.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { WorkspaceConfiguration } from 'vscode'; -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', undefined); - } - public async initialize(): Promise<void> { - if (!this.enabled) { - return; - } - this.initializeSourceMaps(); - const localize = require('./common/utils/localize') as typeof import('./common/utils/localize'); - const disable = localize.Diagnostics.disableSourceMaps(); - const selection = await this.vscode.window.showWarningMessage(localize.Diagnostics.warnSourceMaps(), disable); - if (selection === disable) { - await this.disable(); - } - } - public get enabled(): boolean { - return this.config.get<boolean>(setting, false); - } - public async disable(): Promise<void> { - await this.config.update(setting, false, this.vscode.ConfigurationTarget.Global); - } - protected initializeSourceMaps() { - require('./node_modules/source-map-support').install(); - } -} -// tslint:disable-next-line:no-default-export -export default function initialize(vscode: VSCode) { - new SourceMapSupport(vscode).initialize().catch(ex => { - console.error('Failed to initialize source map support in extension'); - }); -} diff --git a/src/client/startupTelemetry.ts b/src/client/startupTelemetry.ts new file mode 100644 index 000000000000..f7a2a6aea517 --- /dev/null +++ b/src/client/startupTelemetry.ts @@ -0,0 +1,154 @@ +// 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 { ITerminalHelper } from './common/terminal/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 { traceError } from './logging'; +import { EnvironmentType, PythonEnvironment } from './pythonEnvironments/info'; +import { sendTelemetryEvent } from './telemetry'; +import { EventName } from './telemetry/constants'; +import { EditorLoadTelemetry } from './telemetry/types'; +import { IStartupDurations } from './types'; +import { useEnvExtension } from './envExt/api.internal'; + +export async function sendStartupTelemetry( + activatedPromise: Promise<any>, + durations: IStartupDurations, + stopWatch: IStopWatch, + serviceContainer: IServiceContainer, + isFirstSession: boolean, +) { + if (isTestExecution()) { + return; + } + + try { + await activatedPromise; + durations.totalNonBlockingActivateTime = stopWatch.elapsedTime - durations.startActivateTime; + const props = await getActivationTelemetryProps(serviceContainer, isFirstSession); + sendTelemetryEvent(EventName.EDITOR_LOAD, durations, props); + } catch (ex) { + traceError('sendStartupTelemetry() failed.', ex); + } +} + +export async function sendErrorTelemetry( + ex: Error, + durations: IStartupDurations, + serviceContainer?: IServiceContainer, +) { + try { + let props: any = {}; + if (serviceContainer) { + try { + props = await getActivationTelemetryProps(serviceContainer); + } catch (ex) { + traceError('getActivationTelemetryProps() failed.', ex); + } + } + sendTelemetryEvent(EventName.EDITOR_LOAD, durations, props, ex); + } catch (exc2) { + traceError('sendErrorTelemetry() failed.', exc2); + } +} + +function isUsingGlobalInterpreterInWorkspace(currentPythonPath: string, serviceContainer: IServiceContainer): boolean { + const service = serviceContainer.get<IInterpreterAutoSelectionService>(IInterpreterAutoSelectionService); + const globalInterpreter = service.getAutoSelectedInterpreter(undefined); + if (!globalInterpreter) { + return false; + } + return currentPythonPath === globalInterpreter.path; +} + +export function hasUserDefinedPythonPath(resource: Resource, serviceContainer: IServiceContainer) { + const interpreterPathService = serviceContainer.get<IInterpreterPathService>(IInterpreterPathService); + let settings = interpreterPathService.inspect(resource); + return (settings.workspaceFolderValue && settings.workspaceFolderValue !== 'python') || + (settings.workspaceValue && settings.workspaceValue !== 'python') || + (settings.globalValue && settings.globalValue !== 'python') + ? true + : false; +} + +async function getActivationTelemetryProps( + serviceContainer: IServiceContainer, + isFirstSession?: boolean, +): Promise<EditorLoadTelemetry> { + // TODO: Not all of this data is showing up in the database... + + // 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>(IWorkspaceService); + const workspaceFolderCount = workspaceService.workspaceFolders?.length || 0; + const terminalHelper = serviceContainer.get<ITerminalHelper>(ITerminalHelper); + const terminalShellType = terminalHelper.identifyTerminalShell(); + if (!workspaceService.isTrusted) { + return { workspaceFolderCount, terminal: terminalShellType, isFirstSession }; + } + const interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService); + const mainWorkspaceUri = workspaceService.workspaceFolders?.length + ? workspaceService.workspaceFolders[0].uri + : undefined; + 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<PythonEnvironment | undefined>(() => undefined); + } else { + interpreter = await interpreterService + .getActiveInterpreter() + .catch<PythonEnvironment | undefined>(() => 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>(ICondaService); + condaVersion = await condaLocator + .getCondaVersion() + .then((ver) => (ver ? ver.raw : '')) + .catch<string>(() => ''); + } + const usingUserDefinedInterpreter = hasUserDefinedPythonPath(mainWorkspaceUri, serviceContainer); + const usingGlobalInterpreter = interpreter + ? isUsingGlobalInterpreterInWorkspace(interpreter.path, serviceContainer) + : false; + const usingEnvironmentsExtension = useEnvExtension(); + + return { + condaVersion, + terminal: terminalShellType, + pythonVersion, + interpreterType, + workspaceFolderCount, + hasPythonThree, + usingUserDefinedInterpreter, + usingGlobalInterpreter, + appName, + isFirstSession, + usingEnvironmentsExtension, + }; +} diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 04652b62f053..eff32a6e3299 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -3,60 +3,105 @@ 'use strict'; -export const COMPLETION = 'COMPLETION'; -export const COMPLETION_ADD_BRACKETS = 'COMPLETION.ADD_BRACKETS'; -export const DEFINITION = 'DEFINITION'; -export const HOVER_DEFINITION = 'HOVER_DEFINITION'; -export const REFERENCE = 'REFERENCE'; -export const SIGNATURE = 'SIGNATURE'; -export const SYMBOL = 'SYMBOL'; -export const FORMAT_SORT_IMPORTS = 'FORMAT.SORT_IMPORTS'; -export const FORMAT = 'FORMAT.FORMAT'; -export const FORMAT_ON_TYPE = 'FORMAT.FORMAT_ON_TYPE'; -export const EDITOR_LOAD = 'EDITOR.LOAD'; -export const LINTING = 'LINTING'; -export const GO_TO_OBJECT_DEFINITION = 'GO_TO_OBJECT_DEFINITION'; -export const UPDATE_PYSPARK_LIBRARY = 'UPDATE_PYSPARK_LIBRARY'; -export const REFACTOR_RENAME = 'REFACTOR_RENAME'; -export const REFACTOR_EXTRACT_VAR = 'REFACTOR_EXTRACT_VAR'; -export const REFACTOR_EXTRACT_FUNCTION = 'REFACTOR_EXTRACT_FUNCTION'; -export const REPL = 'REPL'; -export const PYTHON_INTERPRETER = 'PYTHON_INTERPRETER'; -export const PYTHON_INTERPRETER_DISCOVERY = 'PYTHON_INTERPRETER_DISCOVERY'; -export const PYTHON_INTERPRETER_AUTO_SELECTION = 'PYTHON_INTERPRETER_AUTO_SELECTION'; -export const WORKSPACE_SYMBOLS_BUILD = 'WORKSPACE_SYMBOLS.BUILD'; -export const WORKSPACE_SYMBOLS_GO_TO = 'WORKSPACE_SYMBOLS.GO_TO'; -export const EXECUTION_CODE = 'EXECUTION_CODE'; -export const EXECUTION_DJANGO = 'EXECUTION_DJANGO'; -export const DEBUGGER = 'DEBUGGER'; -export const DEBUGGER_ATTACH_TO_CHILD_PROCESS = 'DEBUGGER.ATTACH_TO_CHILD_PROCESS'; -export const DEBUGGER_PERFORMANCE = 'DEBUGGER.PERFORMANCE'; -export const DEBUGGER_CONFIGURATION_PROMPTS = 'DEBUGGER.CONFIGURATION.PROMPTS'; -export const UNITTEST_STOP = 'UNITTEST.STOP'; -export const UNITTEST_RUN = 'UNITTEST.RUN'; -export const UNITTEST_DISCOVER = 'UNITTEST.DISCOVER'; -export const UNITTEST_VIEW_OUTPUT = 'UNITTEST.VIEW_OUTPUT'; -export const PYTHON_LANGUAGE_SERVER_ANALYSISTIME = 'PYTHON_LANGUAGE_SERVER.ANALYSIS_TIME'; -export const PYTHON_LANGUAGE_SERVER_ENABLED = 'PYTHON_LANGUAGE_SERVER.ENABLED'; -export const PYTHON_LANGUAGE_SERVER_EXTRACTED = 'PYTHON_LANGUAGE_SERVER.EXTRACTED'; -export const PYTHON_LANGUAGE_SERVER_DOWNLOADED = 'PYTHON_LANGUAGE_SERVER.DOWNLOADED'; -export const PYTHON_LANGUAGE_SERVER_ERROR = 'PYTHON_LANGUAGE_SERVER.ERROR'; -export const PYTHON_LANGUAGE_SERVER_STARTUP = 'PYTHON_LANGUAGE_SERVER.STARTUP'; -export const PYTHON_LANGUAGE_SERVER_PLATFORM_NOT_SUPPORTED = 'PYTHON_LANGUAGE_SERVER.PLATFORM_NOT_SUPPORTED'; -export const PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED = 'PYTHON_LANGUAGE_SERVER.PLATFORM_SUPPORTED'; -export const PYTHON_LANGUAGE_SERVER_TELEMETRY = 'PYTHON_LANGUAGE_SERVER.EVENT'; - -export const TERMINAL_CREATE = 'TERMINAL.CREATE'; -export const PYTHON_LANGUAGE_SERVER_LIST_BLOB_STORE_PACKAGES = 'PYTHON_LANGUAGE_SERVER.LIST_BLOB_PACKAGES'; -export const DIAGNOSTICS_ACTION = 'DIAGNOSTICS.ACTION'; -export const DIAGNOSTICS_MESSAGE = 'DIAGNOSTICS.MESSAGE'; -export const PLATFORM_INFO = 'PLATFORM.INFO'; - -export const SELECT_LINTER = 'LINTING.SELECT'; - -export const LINTER_NOT_INSTALLED_PROMPT = 'LINTER_NOT_INSTALLED_PROMPT'; +export enum EventName { + FORMAT_ON_TYPE = 'FORMAT.FORMAT_ON_TYPE', + EDITOR_LOAD = 'EDITOR.LOAD', + 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_FOR_RUNNING_CODE = 'PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE', + PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL = 'PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL', + 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', + REQUIRE_JUPYTER_PROMPT = 'REQUIRE_JUPYTER_PROMPT', + ACTIVATED_CONDA_ENV_LAUNCH = 'ACTIVATED_CONDA_ENV_LAUNCH', + ENVFILE_VARIABLE_SUBSTITUTION = 'ENVFILE_VARIABLE_SUBSTITUTION', + ENVFILE_WORKSPACE = 'ENVFILE_WORKSPACE', + EXECUTION_CODE = 'EXECUTION_CODE', + EXECUTION_DJANGO = 'EXECUTION_DJANGO', + + // 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_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', + + 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', + + USE_REPORT_ISSUE_COMMAND = 'USE_REPORT_ISSUE_COMMAND', + + HASHED_PACKAGE_NAME = 'HASHED_PACKAGE_NAME', + + 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 new file mode 100644 index 000000000000..bf76a08733f6 --- /dev/null +++ b/src/client/telemetry/envFileTelemetry.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IWorkspaceService } from '../common/application/types'; +import { IFileSystem } from '../common/platform/types'; +import { Resource } from '../common/types'; +import { SystemVariables } from '../common/variables/systemVariables'; + +import { sendTelemetryEvent } from '.'; +import { EventName } from './constants'; + +let _defaultEnvFileSetting: string | undefined; +let envFileTelemetrySent = false; + +export function sendSettingTelemetry(workspaceService: IWorkspaceService, envFileSetting?: string): void { + if (shouldSendTelemetry() && envFileSetting !== defaultEnvFileSetting(workspaceService)) { + sendTelemetry(true); + } +} + +export function sendFileCreationTelemetry(): void { + if (shouldSendTelemetry()) { + sendTelemetry(); + } +} + +export async function sendActivationTelemetry( + fileSystem: IFileSystem, + workspaceService: IWorkspaceService, + resource: Resource, +): Promise<void> { + if (shouldSendTelemetry()) { + const systemVariables = new SystemVariables(resource, undefined, workspaceService); + const envFilePath = systemVariables.resolveAny(defaultEnvFileSetting(workspaceService))!; + const envFileExists = await fileSystem.fileExists(envFilePath); + + if (envFileExists) { + sendTelemetry(); + } + } +} + +function sendTelemetry(hasCustomEnvPath = false) { + sendTelemetryEvent(EventName.ENVFILE_WORKSPACE, undefined, { hasCustomEnvPath }); + + envFileTelemetrySent = true; +} + +function shouldSendTelemetry(): boolean { + return !envFileTelemetrySent; +} + +function defaultEnvFileSetting(workspaceService: IWorkspaceService) { + if (!_defaultEnvFileSetting) { + const section = workspaceService.getConfiguration('python'); + _defaultEnvFileSetting = section.inspect<string>('envFile')?.defaultValue || ''; + } + + return _defaultEnvFileSetting; +} + +// Set state for tests. +export const EnvFileTelemetryTests = { + setState: ({ telemetrySent, defaultSetting }: { telemetrySent?: boolean; defaultSetting?: string }): void => { + if (telemetrySent !== undefined) { + envFileTelemetrySent = telemetrySent; + } + if (defaultEnvFileSetting !== undefined) { + _defaultEnvFileSetting = defaultSetting; + } + }, + 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<void> { + // 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 (<home>, .vscode, extensions), we will + // end up looking at the wrong place, with respect to the extension that was launched. + const fileToCheck = path.join(path.dirname(EXTENSION_ROOT_DIR), 'PythonCodingPack'); + if (await fs.fileExists(fileToCheck)) { + setSharedProperty('installSource', 'pythonCodingPack'); + } else { + // We did not file the `PythonCodingPack` file, assume market place install. + setSharedProperty('installSource', 'marketPlace'); + } +} diff --git a/src/client/telemetry/importTracker.ts b/src/client/telemetry/importTracker.ts new file mode 100644 index 000000000000..cf8e1ed48837 --- /dev/null +++ b/src/client/telemetry/importTracker.ts @@ -0,0 +1,165 @@ +/* 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 { 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 { TorchProfilerImportRegEx } from '../tensorBoard/helpers'; +import { EventName } from './constants'; + +/* +Python has a fairly rich import statement. Originally the matching regexp was kept simple for +performance worries, but it led to false-positives due to matching things like docstrings with +phrases along the lines of "from the thing" or "import the thing". To minimize false-positives the +regexp does its best to validate the structure of the import line _within reason_. This leads to +us supporting the following (where `pkg` represents what we are actually capturing for telemetry): + +- `from pkg import _` +- `from pkg import _, _` +- `from pkg import _ as _` +- `import pkg` +- `import pkg, pkg` +- `import pkg as _` + +Things we are ignoring the following for simplicity/performance: + +- `from pkg import (...)` (this includes single-line and multi-line imports with parentheses) +- `import pkg # ... and anything else with a trailing comment.` +- Non-standard whitespace separators within the import statement (i.e. more than a single space, tabs) + +*/ +const ImportRegEx = /^\s*(from (?<fromImport>\w+)(?:\.\w+)* import \w+(?:, \w+)*(?: as \w+)?|import (?<importImport>\w+(?:, \w+)*)(?: as \w+)?)$/; +const MAX_DOCUMENT_LINES = 1000; + +// Capture isTestExecution on module load so that a test can turn it off and still +// have this value set. +const testExecution = isTestExecution(); + +@injectable() +export class ImportTracker implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + + private pendingChecks = new Map<string, NodeJS.Timeout>(); + + private static sentMatches: Set<string> = new Set<string>(); + + constructor( + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(IDisposableRegistry) private disposables: IDisposableRegistry, + ) { + this.documentManager.onDidOpenTextDocument((t) => this.onOpenedOrSavedDocument(t), this, this.disposables); + this.documentManager.onDidSaveTextDocument((t) => this.onOpenedOrSavedDocument(t), this, this.disposables); + } + + public dispose(): void { + this.pendingChecks.clear(); + } + + public async activate(): Promise<void> { + // 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)); + } + + 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).toLowerCase() === '.py') { + this.scheduleDocument(document); + } + } + + private scheduleDocument(document: TextDocument) { + this.scheduleCheck(document.fileName, this.checkDocument.bind(this, document)); + } + + private scheduleCheck(file: string, check: () => void) { + // If already scheduled, cancel. + const currentTimeout = this.pendingChecks.get(file); + if (currentTimeout) { + clearTimeout(currentTimeout); + this.pendingChecks.delete(file); + } + + // Now schedule a new one. + if (testExecution) { + // During a test, check right away. It needs to be synchronous. + check(); + } else { + // Wait five seconds to make sure we don't already have this document pending. + this.pendingChecks.set(file, setTimeout(check, 5000)); + } + } + + private checkDocument(document: TextDocument) { + this.pendingChecks.delete(document.fileName); + 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 (ImportTracker.sentMatches.has(packageName)) { + return; + } + ImportTracker.sentMatches.add(packageName); + // Hash the package name so that we will never accidentally see a + // user's private package name. + const hash = createHash('sha256').update(packageName).digest('hex'); + sendTelemetryEvent(EventName.HASHED_PACKAGE_NAME, undefined, { hashedName: hash }); + } + + private lookForImports(lines: (string | undefined)[]) { + try { + for (const s of lines) { + const match = s ? ImportRegEx.exec(s) : null; + if (match !== null && match.groups !== undefined) { + if (match.groups.fromImport !== undefined) { + // `from pkg ...` + this.sendTelemetry(match.groups.fromImport); + } else if (match.groups.importImport !== undefined) { + // `import pkg1, pkg2, ...` + const packageNames = match.groups.importImport + .split(',') + .map((rawPackageName) => rawPackageName.trim()); + // Can't pass in `this.sendTelemetry` directly as that rebinds `this`. + packageNames.forEach((p) => this.sendTelemetry(p)); + } + } + if (s && TorchProfilerImportRegEx.test(s)) { + sendTelemetryEvent(EventName.TENSORBOARD_TORCH_PROFILER_IMPORT); + } + } + } catch { + // Don't care about failures since this is just telemetry. + noop(); + } + } +} + +export function getDocumentLines(document: TextDocument): (string | undefined)[] { + const array = Array<string>(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 396d15471c4b..763f7405aa0d 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1,112 +1,251 @@ +/* eslint-disable global-require */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// tslint:disable-next-line:no-reference -/// <reference path="./vscode-extension-telemetry.d.ts" /> -// tslint:disable-next-line:import-name -import TelemetryReporter from 'vscode-extension-telemetry'; -import { isTestExecution, PVSC_EXTENSION_ID } from '../common/constants'; +import TelemetryReporter from '@vscode/extension-telemetry'; +import type * as vscodeTypes from 'vscode'; +import { DiagnosticCodes } from '../application/diagnostics/constants'; +import { AppinsightsKey, isTestExecution, isUnitTestExecution, PVSC_EXTENSION_ID } from '../common/constants'; +import type { TerminalShellType } from '../common/terminal/types'; +import { isPromise } from '../common/utils/async'; import { StopWatch } from '../common/utils/stopWatch'; -import { TelemetryProperties } from './types'; +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. - * Withiin DA, there's a completely different way to send telemetry. - * @returns {boolean} + * Within DA, there's a completely different way to send telemetry. */ 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; } } -let telemetryReporter: TelemetryReporter; -function getTelemetryReporter() { - if (telemetryReporter) { + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let packageJSON: any; + +/** + * Checks if the telemetry is disabled + */ +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<string, unknown> = {}; +/** + * Set shared properties for all telemetry events. + */ +export function setSharedProperty<P extends ISharedPropertyMapping, E extends keyof P>(name: E, value?: P[E]): void { + const propertyName = name as string; + // Ignore such shared telemetry during unit tests. + if (isUnitTestExecution() && propertyName.startsWith('ds_')) { + return; + } + if (value === undefined) { + delete sharedProperties[propertyName]; + } else { + sharedProperties[propertyName] = value; + } +} + +/** + * Reset shared properties for testing purposes. + */ +export function _resetSharedProperties(): void { + for (const key of Object.keys(sharedProperties)) { + delete sharedProperties[key]; + } +} + +let telemetryReporter: TelemetryReporter | undefined; +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; - // tslint:disable-next-line:no-non-null-assertion - const extension = extensions.getExtension(extensionId)!; - // tslint:disable-next-line:no-unsafe-any - const extensionVersion = extension.packageJSON.version; - // tslint:disable-next-line:no-unsafe-any - const aiKey = extension.packageJSON.contributes.debuggers[0].aiKey; - - // tslint:disable-next-line:no-require-imports - const reporter = require('vscode-extension-telemetry').default as typeof TelemetryReporter; - return telemetryReporter = new reporter(extensionId, extensionVersion, aiKey); + + const Reporter = require('@vscode/extension-telemetry').default as typeof TelemetryReporter; + telemetryReporter = new Reporter(AppinsightsKey, [ + { + lookup: /(errorName|errorMessage|errorStack)/g, + }, + ]); + + return telemetryReporter; +} + +export function clearTelemetryReporter(): void { + telemetryReporter = undefined; } -export function sendTelemetryEvent(eventName: string, durationMs?: { [key: string]: number } | number, properties?: TelemetryProperties) { - if (isTestExecution() || !isTelemetrySupported()) { +export function sendTelemetryEvent<P extends IEventNamePropertyMapping, E extends keyof P>( + eventName: E, + measuresOrDurationMs?: Record<string, number> | number, + properties?: P[E], + ex?: Error, +): void { + if (isTestExecution() || !isTelemetrySupported() || isTelemetryDisabled()) { return; } const reporter = getTelemetryReporter(); - const measures = typeof durationMs === 'number' ? { duration: durationMs } : (durationMs ? durationMs : undefined); + const measures = + typeof measuresOrDurationMs === 'number' + ? { duration: measuresOrDurationMs } + : measuresOrDurationMs || undefined; + const customProperties: Record<string, string> = {}; + const eventNameSent = eventName as string; - // tslint:disable-next-line:no-any - const customProperties: { [key: string]: string } = {}; if (properties) { - // tslint:disable-next-line:prefer-type-cast no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any const data = properties as any; - Object.getOwnPropertyNames(data).forEach(prop => { + Object.getOwnPropertyNames(data).forEach((prop) => { if (data[prop] === undefined || data[prop] === null) { return; } - // tslint:disable-next-line:prefer-type-cast no-any no-unsafe-any - (customProperties as any)[prop] = typeof data[prop] === 'string' ? data[prop] : data[prop].toString(); + 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 ${String(eventName)}`, exception); // use console due to circular dependencies with trace calls + } }); } - reporter.sendTelemetryEvent(eventName, properties ? customProperties : undefined, measures); + + // 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) { + console.info( + `Telemetry Event : ${eventNameSent} Measures: ${JSON.stringify(measures)} Props: ${JSON.stringify( + customProperties, + )} `, + ); // use console due to circular dependencies with trace calls + } } -// tslint:disable-next-line:no-any function-name -export function captureTelemetry( - eventName: string, - properties?: TelemetryProperties, - captureDuration: boolean = true, - failureEventName?: string -) { - // tslint:disable-next-line:no-function-expression no-any - return function (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) { - const originalMethod = descriptor.value; - // tslint:disable-next-line:no-function-expression no-any - descriptor.value = function (...args: any[]) { - if (!captureDuration) { +// Type-parameterized form of MethodDecorator in lib.es5.d.ts. +type TypedMethodDescriptor<T> = ( + target: unknown, + propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor<T>, +) => TypedPropertyDescriptor<T> | 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. + * @param properties Properties to send with the event; must be valid for the event. + * @param captureDuration True if the method's execution duration should be captured. + * @param failureEventName If the decorated method returns a Promise and fails, send this event instead of eventName. + * @param lazyProperties A static function on the decorated class which returns extra properties to add to the event. + * This can be used to provide properties which are only known at runtime (after the decorator has executed). + * @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). + */ +export function captureTelemetry<This, P extends IEventNamePropertyMapping, E extends keyof P>( + eventName: E, + properties?: P[E], + captureDuration = true, + failureEventName?: 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<string, number>, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): TypedMethodDescriptor<(this: This, ...args: any[]) => any> { + return function ( + _target: unknown, + _propertyKey: string | symbol, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor: TypedPropertyDescriptor<(this: This, ...args: any[]) => any>, + ) { + const originalMethod = descriptor.value!; + + // 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 && !lazyMeasures) { sendTelemetryEvent(eventName, undefined, properties); - // tslint:disable-next-line:no-invalid-this + return originalMethod.apply(this, args); } - const stopWatch = new StopWatch(); - // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const getProps = (result?: any) => { + if (lazyProperties) { + return { ...properties, ...lazyProperties(this, result) }; + } + return properties; + }; + + const stopWatch = captureDuration ? new StopWatch() : undefined; + + // 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<void>) - .then(data => { - sendTelemetryEvent(eventName, stopWatch.elapsedTime, properties); + if (result && isPromise(result)) { + result + .then((data) => { + sendTelemetryEvent(eventName, getMeasures(data), getProps(data)); return data; }) - // tslint:disable-next-line:promise-function-async - .catch(ex => { - // tslint:disable-next-line:no-any - sendTelemetryEvent(failureEventName ? failureEventName : eventName, stopWatch.elapsedTime, properties); + .catch((ex) => { + const failedProps: P[E] = { ...getProps(), failed: true } as P[E] & FailedEventType; + sendTelemetryEvent(failureEventName || eventName, getMeasures(), failedProps, ex); }); } else { - sendTelemetryEvent(eventName, stopWatch.elapsedTime, properties); + sendTelemetryEvent(eventName, getMeasures(result), getProps(result)); } return result; @@ -116,24 +255,2262 @@ export function captureTelemetry( }; } -// tslint:disable-next-line:no-any function-name -export function sendTelemetryWhenDone(eventName: string, promise: Promise<any> | Thenable<any>, - stopWatch?: StopWatch, properties?: TelemetryProperties) { - stopWatch = stopWatch ? stopWatch : new StopWatch(); +// function sendTelemetryWhenDone<T extends IDSMappings, K extends keyof T>(eventName: K, properties?: T[K]); +export function sendTelemetryWhenDone<P extends IEventNamePropertyMapping, E extends keyof P>( + eventName: E, + promise: Promise<unknown> | Thenable<unknown>, + stopWatch?: 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<any>) - .then(data => { - // tslint:disable-next-line:no-non-null-assertion + (promise as Promise<unknown>).then( + (data) => { 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) => { + sendTelemetryEvent(eventName, stopWatch!.elapsedTime, properties, ex); return Promise.reject(ex); - }); + }, + ); } else { throw new Error('Method is neither a Promise nor a Theneable'); } } + +/** + * Map all shared properties to their data types. + */ +export interface ISharedPropertyMapping { + /** + * For every DS telemetry we would like to know the type of Notebook Editor used when doing something. + */ + ['ds_notebookeditor']: undefined | 'old' | 'custom' | 'native'; + + /** + * For every telemetry event from the extension we want to make sure we can associate it with install + * source. We took this approach to work around very limiting query performance issues. + */ + ['installSource']: undefined | 'marketPlace' | 'pythonCodingPack'; +} + +type FailedEventType = { failed: true }; + +// Map all events to their properties +export interface IEventNamePropertyMapping { + [EventName.DIAGNOSTICS_ACTION]: { + /** + * Diagnostics command executed. + * @type {string} + */ + commandName?: string; + /** + * Diagnostisc code ignored (message will not be seen again). + * @type {string} + */ + ignoreCode?: string; + /** + * Url of web page launched in browser. + * @type {string} + */ + url?: string; + /** + * Custom actions performed. + * @type {'switchToCommandPrompt'} + */ + action?: 'switchToCommandPrompt'; + }; + /** + * Telemetry event sent when we are checking if we can handle the diagnostic code + */ + /* __GDPR__ + "diagnostics.message" : { + "code" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.DIAGNOSTICS_MESSAGE]: { + /** + * Code of diagnostics message detected and displayed. + * @type {string} + */ + code: DiagnosticCodes; + }; + /** + * 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; + /** + * The python interpreter version if selected + */ + pythonVersion?: string | undefined; + /** + * The type of interpreter (conda, virtualenv, pipenv etc.) + */ + interpreterType?: EnvironmentType | undefined; + /** + * The type of terminal shell created: powershell, cmd, zsh, bash etc. + * + * @type {TerminalShellType} + */ + terminal: TerminalShellType; + /** + * Number of workspace folders opened + */ + workspaceFolderCount: number; + /** + * If interpreters found for the main workspace contains a python3 interpreter + */ + hasPythonThree?: boolean; + /** + * If user has defined an interpreter in settings.json + */ + usingUserDefinedInterpreter?: boolean; + /** + * If global interpreter is being used + */ + usingGlobalInterpreter?: boolean; + /** + * 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 + */ + 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. + */ + hasCustomEnvPath: boolean; + }; + /** + * 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 or line by shift+enter. + * + * @type {('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. + * Values sent: + * 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. + * If `selection`, then the selected text was sent to the django shell. + * + * @type {('file' | 'selection')} + */ + scope: 'file' | 'selection'; + }; + + /** + * 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 + * + * @type {boolean} + */ + enabled: boolean; + }; + + /** + * 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 + * + * @type {string} + */ + hashedName: string; + }; + + /** + * 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.) + * 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; + /** + * The name of the installer required (expected to be available) for installation of packages. (pipenv, Conda etc.) + */ + requiredInstaller?: string; + /** + * Name of the corresponding product (package) to be installed. + */ + productName?: string; + /** + * Whether the product (package) has been installed or not. + */ + isInstalled?: boolean; + /** + * Type of the Python environment into which the Python package is being installed. + */ + envType?: PythonEnvironment['envType']; + /** + * Version of the Python environment into which the Python package is being installed. + */ + version?: string; + }; + /** + * Telemetry event sent when an environment without contain a python binary is selected. + */ + /* __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. + * Carries 'browse' if user chose to browse for the path to the executable. + */ + 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 + * + * @type {('ui' | 'shebang' | 'load')} + */ + trigger: 'ui' | 'shebang' | 'load'; + /** + * Carries `true` if updating python interpreter failed + * + * @type {boolean} + */ + failed: boolean; + /** + * The python version of the interpreter + * + * @type {string} + */ + pythonVersion?: string; + }; + /* __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 + * + * @type {boolean} + */ + hasEnvVars?: boolean; + /** + * Carries `true` if fetching environment variables failed, `false` otherwise + * + * @type {boolean} + */ + failed?: 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 + * + * @type {boolean} + */ + hasCommands?: boolean; + /** + * Carries `true` if fetching activation commands for interpreter failed, `false` otherwise + * + * @type {boolean} + */ + failed?: boolean; + /** + * The type of terminal shell to activate + * + * @type {TerminalShellType} + */ + terminal: TerminalShellType; + /** + * The Python interpreter version of the active interpreter for the resource + * + * @type {string} + */ + pythonVersion?: string; + /** + * The type of the interpreter used + * + * @type {EnvironmentType} + */ + interpreterType: EnvironmentType; + }; + /** + * Telemetry event sent when getting activation commands for terminal when interpreter is not specified + */ + /* __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 + * + * @type {boolean} + */ + hasCommands?: boolean; + /** + * Carries `true` if fetching activation commands for terminal failed, `false` otherwise + * + * @type {boolean} + */ + failed?: boolean; + /** + * The type of terminal shell to activate + * + * @type {TerminalShellType} + */ + terminal: TerminalShellType; + /** + * The Python interpreter version of the interpreter for the resource + * + * @type {string} + */ + pythonVersion?: string; + /** + * The type of the interpreter used + * + * @type {EnvironmentType} + */ + interpreterType: EnvironmentType; + }; + /** + * 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]: { + /** + * If auto-selection has been run earlier in this session, and this call returned a cached value. + * + * @type {boolean} + */ + 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]: { + /** + * Version of this telemetry. + */ + telVer?: number; + /** + * Number of invalid envs returned by `conda info` + */ + condaInfoEnvsInvalid?: number; + /** + * Number of conda envs found in the environments.txt file. + */ + condaEnvsInTxt?: number; + /** + * Number of duplicate envs returned by `conda info` + */ + condaInfoEnvsDuplicate?: number; + /** + * Number of envs with invalid prefix returned by `conda info` + */ + condaInfoEnvsInvalidPrefix?: number; + /** + * Number of workspaces. + */ + workspaceFolderCount?: number; + /** + * 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 Native finder fails to find some conda envs. + */ + /* __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.NATIVE_FINDER_MISSING_CONDA_ENVS]: { + /** + * Number of missing conda environments. + */ + missing: number; + /** + * 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. + */ + envDirsNotFound?: number; + /** + * Whether a conda exe was provided by the user. + */ + userProvidedCondaExe?: boolean; + /** + * Whether the user provided a conda executable. + */ + rootPrefixNotFound?: boolean; + /** + * Whether the conda prefix returned by conda was not found by us. + */ + condaPrefixNotFound?: boolean; + /** + * Whether we found a conda manager or not. + */ + condaManagerNotFound?: boolean; + /** + * Whether we failed to find the system rc path. + */ + sysRcNotFound?: boolean; + /** + * Whether we failed to find the user rc path. + */ + userRcNotFound?: boolean; + /** + * Number of config files (excluding sys and user rc) that were not found. + */ + otherRcNotFound?: boolean; + /** + * Number of conda envs that were not found by us, and the envs belong to env_dirs in the sys config rc. + */ + 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 when Native finder fails to find some conda envs. + */ + /* __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]: { + /** + * Number of missing poetry environments. + */ + missing: number; + /** + * Total number of missing envs, where the envs are created in the virtualenvs_path directory. + */ + missingInPath: number; + /** + * Whether a poetry exe was provided by the user. + */ + userProvidedPoetryExe?: boolean; + /** + * Whether poetry exe was not found. + */ + poetryExeNotFound?: boolean; + /** + * Whether poetry config was not found. + */ + 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 containing performance metrics for Native Finder. + */ + /* __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.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; + /** + * Time taken to find all Pyenv environments. + */ + 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 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_INTERPRETER_DISCOVERY_INVALID_NATIVE]: { + /** + * Number of Python envs of a particular type that have invalid version from Native Locator. + */ + invalidVersionsCondaEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid version from Native Locator. + */ + 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; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixVirtualEnvEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixVirtualEnvWrapperEnvs?: number; + /** + * Number of Python envs of a particular type that have invalid sys prefix from Native Locator. + */ + invalidSysPrefixOtherGlobalEnvs?: number; + }; + /** + * 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?' + */ + /* __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; + }; + + /** + * 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.REQUIRE_JUPYTER_PROMPT]: { + /** + * `Yes` When 'Yes' option is selected + * `No` When 'No' option is selected + * `undefined` When 'x' is selected + */ + 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]: { + /** + * `Yes` When 'Yes' option is selected + * `No` When 'No' option is selected + */ + selection: 'Yes' | 'No' | 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?' + */ + /* __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 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.' + */ + /* __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 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.USE_REPORT_ISSUE_COMMAND]: unknown; + /** + * Telemetry event sent when the New Python File command is executed. + */ + /* __GDPR__ + "create_new_file_command" : { "owner": "luabud" } + */ + [EventName.CREATE_NEW_FILE_COMMAND]: unknown; + /** + * 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. + */ + + /* __GDPR__ + "python_experiments_lsp_notebooks" : { "owner": "luabud" } + */ + [EventName.PYTHON_EXPERIMENTS_LSP_NOTEBOOKS]: unknown; + /** + * Telemetry event sent once on session start with details on which experiments are opted into and opted out from. + */ + /* __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]: { + /** + * List of valid experiments in the python.experiments.optInto setting + * @type {string} + */ + optedInto: string; + /** + * List of valid experiments in the python.experiments.optOutFrom setting + * @type {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) + */ + /* __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. + */ + /* __GDPR__ + "language_server_request" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.LANGUAGE_SERVER_REQUEST]: unknown; + /** + * Telemetry send when Language Server is restarted. + */ + /* __GDPR__ + "language_server_restart" : { + "reason" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.LANGUAGE_SERVER_RESTART]: { + reason: 'command' | 'settings' | 'notebooksExperiment'; + }; + /** + * Telemetry event sent when Jedi Language Server is started for workspace (workspace folder in case of multi-root) + */ + /* __GDPR__ + "jedi_language_server.enabled" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.JEDI_LANGUAGE_SERVER_ENABLED]: { + lsVersion?: string; + }; + /** + * Telemetry event sent when Jedi Language Server server is ready to receive messages + */ + /* __GDPR__ + "jedi_language_server.ready" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.JEDI_LANGUAGE_SERVER_READY]: { + lsVersion?: string; + }; + /** + * Telemetry event sent when starting Node.js server + */ + /* __GDPR__ + "jedi_language_server.startup" : { + "lsversion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.JEDI_LANGUAGE_SERVER_STARTUP]: { + lsVersion?: string; + }; + /** + * 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.JEDI_LANGUAGE_SERVER_REQUEST]: unknown; + /** + * When user clicks a button in the python extension survey prompt, this telemetry event is sent with details + */ + /* __GDPR__ + "extension_survey_prompt" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ + [EventName.EXTENSION_SURVEY_PROMPT]: { + /** + * Carries the selection of user when they are asked to take the extension survey + */ + selection: 'Yes' | 'Maybe later' | "Don't show again" | undefined; + }; + /** + * Telemetry event sent when starting REPL + */ + /* __GDPR__ + "repl" : { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "anthonykim1" }, + "repltype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "anthonykim1" } + } + */ + [EventName.REPL]: { + /** + * Whether the user launched the Terminal REPL or Native REPL + * + * 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`. + */ + replType: 'Terminal' | 'Native' | 'manualTerminal' | `runningScript`; + }; + /** + * Telemetry event sent when invoking a Tool + */ + /* __GDPR__ + "INVOKE_TOOL" : { + "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" }, + "resolveOutcome": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"Which code path resolved the environment in configure_python_environment.", "owner": "donjayamanne" }, + "envType": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"The type of Python environment (e.g. venv, conda, system).", "owner": "donjayamanne" }, + "packageCount": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"Number of packages requested for installation (install_python_packages only).", "owner": "donjayamanne" }, + "installerType": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"Which installer was used: pip or conda (install_python_packages only).", "owner": "donjayamanne" }, + "responsePackageCount": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"Number of packages in the environment response (get_python_environment_details only).", "owner": "donjayamanne" } + } + */ + [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; + /** + * Which code path resolved the environment (configure_python_environment only). + */ + resolveOutcome?: string; + /** + * The type of Python environment (e.g. venv, conda, system). + */ + envType?: string; + /** + * Number of packages requested for installation (install_python_packages only). + */ + packageCount?: string; + /** + * Which installer was used: pip or conda (install_python_packages only). + */ + installerType?: string; + /** + * Number of packages in the environment response (get_python_environment_details only). + */ + responsePackageCount?: 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 + */ + tool?: TestTool; + /** + * Carries the source which triggered configuration of tests + * + * @type {('ui' | 'commandpalette')} + */ + trigger: 'ui' | 'commandpalette'; + /** + * Carries `true` if configuring test framework failed, `false` otherwise + * + * @type {boolean} + */ + failed: boolean; + }; + /** + * Telemetry event sent when the extension is activated, if an active terminal is present and + * the `python.terminal.activateEnvInCurrentTerminal` setting is set to `true`. + */ + /* __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 + */ + isTerminalVisible?: boolean; + }; + /** + * 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. + * + * @type {TerminalShellType} + */ + terminal?: TerminalShellType; + /** + * The source which triggered creation of terminal + * + * @type {'commandpalette'} + */ + triggeredBy?: 'commandpalette'; + /** + * The default Python interpreter version to be used in terminal, inferred from resource's 'settings.json' + * + * @type {string} + */ + pythonVersion?: string; + /** + * The Python interpreter type: Conda, Virtualenv, Venv, Pipenv etc. + * + * @type {EnvironmentType} + */ + interpreterType?: EnvironmentType; + }; + /** + * Telemetry event sent indicating the trigger source for discovery. + */ + /* __GDPR__ + "unittest.discovery.trigger" : { + "trigger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventName.UNITTEST_DISCOVERY_TRIGGER]: { + /** + * Carries the source which triggered discovering of tests + * + * @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. + */ + trigger: 'auto' | 'ui' | 'commandpalette' | 'watching' | 'interpreter'; + }; + /** + * Telemetry event sent with details about discovering tests + */ + /* __GDPR__ + "unittest.discovering" : { + "tool" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventName.UNITTEST_DISCOVERING]: { + /** + * The test framework used to discover tests + * + * @type {TestTool} + */ + 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]: { + /** + * The test framework used to discover tests + * + * @type {TestTool} + */ + tool: TestTool; + /** + * Carries `true` if discovering tests failed, `false` otherwise + * + * @type {boolean} + */ + failed: boolean; + }; + /** + * Telemetry event sent when cancelling discovering tests + */ + /* __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 `true` if debugging, `false` otherwise + */ + debugging: boolean; + }; + /** + * Telemetry event sent when cancelling running tests + */ + /* __GDPR__ + "unittest.run.stop" : { "owner": "eleanorjboyd" } + */ + [EventName.UNITTEST_RUN_STOP]: never | undefined; + /** + * Telemetry event sent when run all failed test command is triggered + */ + /* __GDPR__ + "unittest.run.all_failed" : { "owner": "eleanorjboyd" } + */ + [EventName.UNITTEST_RUN_ALL_FAILED]: never | undefined; + /** + * Telemetry event sent when testing is disabled for a workspace. + */ + /* __GDPR__ + "unittest.disabled" : { "owner": "eleanorjboyd" } + */ + [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. + This impacts executing code in terminals and activation of environments in terminal. + So, the better this works, the better it is for the user. + failed - If true, indicates we have failed to identify the shell. Note this impacts impacts ability to activate environments in the terminal & code. + shellIdentificationSource - How was the shell identified. One of 'terminalName' | 'settings' | 'environment' | 'default' + If terminalName, then this means we identified the type of the shell based on the name of the terminal. + If settings, then this means we identified the type of the shell based on user settings in VS Code. + If environment, then this means we identified the type of the shell based on their environment (env variables, etc). + I.e. their default OS Shell. + If default, then we reverted to OS defaults (cmd on windows, and bash on the rest). + This is the worst case scenario. + I.e. we could not identify the shell at all. + terminalProvided - If true, we used the terminal provided to detec the shell. If not provided, we use the default shell on user machine. + hasCustomShell - If undefined (not set), we didn't check. + If true, user has customzied their shell in VSC Settings. + hasShellInEnv - If undefined (not set), we didn't check. + If true, user has a shell in their environment. + If false, user does not have a shell in their environment. + */ + /* __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; + shellIdentificationSource: 'terminalName' | 'settings' | 'environment' | 'default' | 'vscode'; + hasCustomShell: undefined | boolean; + hasShellInEnv: undefined | boolean; + }; + /** + * Telemetry event sent when getting environment variables for an activated environment has failed. + * + * @type {(undefined | never)} + * @memberof IEventNamePropertyMapping + */ + /* __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`. + * + * @type {boolean} + */ + isPossiblyCondaEnv: boolean; + /** + * The type of terminal shell created: powershell, cmd, zsh, bash etc. + * + * @type {TerminalShellType} + */ + terminal: TerminalShellType; + }; + + // TensorBoard integration events + /** + * Telemetry event sent when the user is prompted to install Python packages that are + * dependencies for launching an integrated TensorBoard session. + */ + /* __GDPR__ + "tensorboard.session_duration" : { "owner": "donjayamanne" } + */ + [EventName.TENSORBOARD_INSTALL_PROMPT_SHOWN]: never | undefined; + /** + * 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'. + */ + /* __GDPR__ + "tensorboard.install_prompt_selection" : { + "selection" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" }, + "operationtype" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "donjayamanne" } + } + */ + [EventName.TENSORBOARD_INSTALL_PROMPT_SELECTION]: { + selection: TensorBoardPromptSelection; + operationType: 'install' | 'upgrade'; + }; + /** + * Telemetry event sent when we find an active integrated terminal running tensorboard. + */ + /* __GDPR__ + "tensorboard_detected_in_integrated_terminal" : { "owner": "donjayamanne" } + */ + [EventName.TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL]: never | undefined; + /** + * 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. + */ + /* __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; + }; + /** + * 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`. + */ + /* __GDPR__ + "tensorboard.torch_profiler_import" : { "owner": "donjayamanne" } + */ + [EventName.TENSORBOARD_TORCH_PROFILER_IMPORT]: never | undefined; + [EventName.TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL]: never | undefined; + /** + * Telemetry event sent before creating an environment. + */ + /* __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 after creating an environment, but before attempting package installation. + */ + /* __GDPR__ + "environment.created" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_CREATED]: { + environmentType: 'venv' | 'conda' | 'microvenv'; + reason: 'created' | 'existing'; + }; + /** + * Telemetry event sent if creating an environment failed. + */ + /* __GDPR__ + "environment.failed" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "reason" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_FAILED]: { + environmentType: 'venv' | 'conda' | 'microvenv'; + reason: 'noVenv' | 'noPip' | 'noDistUtils' | 'other'; + }; + /** + * Telemetry event sent before installing packages. + */ + /* __GDPR__ + "environment.installing_packages" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "using" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_INSTALLING_PACKAGES]: { + environmentType: 'venv' | 'conda' | 'microvenv'; + using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml' | 'pipUpgrade' | 'pipInstall' | 'pipDownload'; + }; + /** + * Telemetry event sent after installing packages. + */ + /* __GDPR__ + "environment.installed_packages" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "using" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_INSTALLED_PACKAGES]: { + environmentType: 'venv' | 'conda'; + using: 'requirements.txt' | 'pyproject.toml' | 'environment.yml' | 'pipUpgrade'; + }; + /** + * Telemetry event sent if installing packages failed. + */ + /* __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'; + }; + /** + * Telemetry event sent if create environment button was used to trigger the command. + */ + /* __GDPR__ + "environment.button" : {"owner": "karthiknadig" } + */ + [EventName.ENVIRONMENT_BUTTON]: never | undefined; + /** + * Telemetry event if user selected to delete the existing environment. + */ + /* __GDPR__ + "environment.delete" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "status" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_DELETE]: { + environmentType: 'venv' | 'conda'; + status: 'triggered' | 'deleted' | 'failed'; + }; + /** + * Telemetry event if user selected to re-use the existing environment. + */ + /* __GDPR__ + "environment.reuse" : { + "environmentType" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_REUSE]: { + environmentType: 'venv' | 'conda'; + }; + /** + * Telemetry event sent when a check for environment creation conditions is triggered. + */ + /* __GDPR__ + "environment.check.trigger" : { + "trigger" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_CHECK_TRIGGER]: { + trigger: + | 'run-in-terminal' + | 'debug-in-terminal' + | 'run-selection' + | 'on-workspace-load' + | 'as-command' + | 'debug'; + }; + /** + * Telemetry event sent when a check for environment creation condition is computed. + */ + /* __GDPR__ + "environment.check.result" : { + "result" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" } + } + */ + [EventName.ENVIRONMENT_CHECK_RESULT]: { + result: 'criteria-met' | 'criteria-not-met' | 'already-ran' | 'turned-off' | 'no-uri'; + }; + /** + * Telemetry event sent when `pip install` was called from a global env in a shell where shell inegration is supported. + */ + /* __GDPR__ + "environment.terminal.global_pip" : { "owner": "karthiknadig" } + */ + [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 3242aad30776..42e51b261129 100644 --- a/src/client/telemetry/types.ts +++ b/src/client/telemetry/types.ts @@ -1,191 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - -import { TerminalShellType } from '../common/terminal/types'; -import { DebugConfigurationType } from '../debugger/extension/types'; -import { AutoSelectionRule } from '../interpreter/autoSelection/types'; -import { InterpreterType } from '../interpreter/contracts'; -import { LinterId } from '../linters/types'; -import { PlatformErrors } from './constants'; - -export type EditorLoadTelemetry = { - condaVersion: string | undefined; - terminal: TerminalShellType; - hasUserDefinedInterpreter: boolean; - isAutoSelectedWorkspaceInterpreterUsed: boolean; -}; -export type FormatTelemetry = { - tool: 'autopep8' | 'black' | 'yapf'; - hasCustomArgs: boolean; - formatSelection: boolean; -}; - -export type LanguageServerVersionTelemetry = { - success: boolean; - lsVersion?: string; -}; - -export type LanguageServerErrorTelemetry = { - error: string; -}; - -export type LanguageServePlatformSupported = { - supported: boolean; - failureType?: 'UnknownError'; -}; -export type LinterTrigger = 'auto' | 'save'; - -export type LintingTelemetry = { - tool: LinterId; - hasCustomArgs: boolean; - trigger: LinterTrigger; - executableSpecified: boolean; -}; - -export type LinterInstallPromptTelemetry = { - tool?: LinterId; - action: 'select'|'disablePrompt'|'install'; -}; - -export type LinterSelectionTelemetry = { - tool?: LinterId; - enabled: boolean; -}; - -export type PythonInterpreterTelemetry = { - trigger: 'ui' | 'shebang' | 'load'; - failed: boolean; - pythonVersion?: string; - pipVersion?: string; -}; -export type CodeExecutionTelemetry = { - scope: 'file' | 'selection'; -}; -export type DebuggerTelemetry = { - trigger: 'launch' | 'attach'; - console?: 'none' | 'integratedTerminal' | 'externalTerminal'; - hasEnvVars: boolean; - hasArgs: boolean; - django: boolean; - flask: boolean; - jinja: boolean; - isLocalhost: boolean; - isModule: boolean; - isSudo: boolean; - stopOnEntry: boolean; - showReturnValue: boolean; - pyramid: boolean; - subProcess: boolean; - watson: boolean; - pyspark: boolean; - gevent: boolean; - scrapy: boolean; -}; -export type DebuggerPerformanceTelemetry = { - duration: number; - action: 'stepIn' | 'stepOut' | 'continue' | 'next' | 'launch'; -}; -export type TestRunTelemetry = { - tool: 'nosetest' | 'pytest' | 'unittest'; - scope: 'currentFile' | 'all' | 'file' | 'class' | 'function' | 'failed'; - debugging: boolean; - triggerSource: 'ui' | 'codelens' | 'commandpalette' | 'auto'; - failed: boolean; -}; -export type TestDiscoverytTelemetry = { - tool: 'nosetest' | 'pytest' | 'unittest'; - trigger: 'ui' | 'commandpalette'; - failed: boolean; -}; -export type FeedbackTelemetry = { - action: 'accepted' | 'dismissed' | 'doNotShowAgain'; -}; -export type SettingsTelemetry = { - enabled: boolean; -}; -export type TerminalTelemetry = { - terminal?: TerminalShellType; - triggeredBy?: 'commandpalette'; - pythonVersion?: string; - interpreterType?: InterpreterType; -}; -export type DebuggerConfigurationPromtpsTelemetry = { - configurationType: DebugConfigurationType; - autoDetectedDjangoManagePyPath?: boolean; - autoDetectedPyramidIniPath?: boolean; - autoDetectedFlaskAppPyPath?: boolean; - manuallyEnteredAValue?: boolean; -}; -export type DiagnosticsAction = { - /** - * Diagnostics command executed. - * @type {string} - */ - commandName?: string; - /** - * Diagnostisc code ignored (message will not be seen again). - * @type {string} - */ - ignoreCode?: string; - /** - * Url of web page launched in browser. - * @type {string} - */ - url?: string; - /** - * Custom actions performed. - * @type {'switchToCommandPrompt'} - */ - action?: 'switchToCommandPrompt'; -}; -export type DiagnosticsMessages = { - /** - * Code of diagnostics message detected and displayed. - * @type {string} - */ - code: string; -}; -export type ImportNotebook = { - scope: 'command'; -}; +'use strict'; -export type Platform = { - failureType?: PlatformErrors; - osVersion?: string; -}; +import type { IEventNamePropertyMapping } from './index'; +import { EventName } from './constants'; -export type InterpreterAutoSelection = { - rule?: AutoSelectionRule; - interpreterMissing?: boolean; - identified?: boolean; - updated?: boolean; -}; -export type InterpreterDiscovery = { - locator: string; -}; +export type EditorLoadTelemetry = IEventNamePropertyMapping[EventName.EDITOR_LOAD]; -export type TelemetryProperties = FormatTelemetry - | LanguageServerVersionTelemetry - | LanguageServerErrorTelemetry - | LintingTelemetry - | LinterInstallPromptTelemetry - | LinterSelectionTelemetry - | EditorLoadTelemetry - | PythonInterpreterTelemetry - | CodeExecutionTelemetry - | TestRunTelemetry - | TestDiscoverytTelemetry - | FeedbackTelemetry - | TerminalTelemetry - | DebuggerTelemetry - | SettingsTelemetry - | DiagnosticsAction - | DiagnosticsMessages - | ImportNotebook - | Platform - | LanguageServePlatformSupported - | DebuggerConfigurationPromtpsTelemetry - | InterpreterAutoSelection - | InterpreterDiscovery; +export type PythonInterpreterTelemetry = IEventNamePropertyMapping[EventName.PYTHON_INTERPRETER]; +export type TestTool = 'pytest' | 'unittest'; +export type TestRunTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_RUN]; +export type TestDiscoveryTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_DONE]; +export type TestConfiguringTelemetry = IEventNamePropertyMapping[EventName.UNITTEST_CONFIGURING]; +export const IImportTracker = Symbol('IImportTracker'); +export interface IImportTracker {} diff --git a/src/client/telemetry/vscode-extension-telemetry.d.ts b/src/client/telemetry/vscode-extension-telemetry.d.ts deleted file mode 100644 index 6a53430a0f28..000000000000 --- a/src/client/telemetry/vscode-extension-telemetry.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -declare module 'vscode-extension-telemetry' { - export default class TelemetryReporter { - /** - * Constructs a new telemetry reporter - * @param {string} extensionId All events will be prefixed with this event name - * @param {string} extensionVersion Extension version to be reported with each event - * @param {string} key The application insights key - */ - // tslint:disable-next-line:no-empty - constructor(extensionId: string, extensionVersion: string, key: string); - - /** - * Sends a telemetry event - * @param {string} eventName The event name - * @param {object} properties An associative array of strings - * @param {object} measures An associative array of numbers - */ - // tslint:disable-next-line:member-access - public sendTelemetryEvent(eventName: string, properties?: { - [key: string]: string; - }, measures?: { - [key: string]: number; - // tslint:disable-next-line:no-empty - }): void; - } -} 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, 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<boolean>; + + constructor(@inject(IPersistentStateFactory) private persistentStateFactory: IPersistentStateFactory) { + this.state = this.persistentStateFactory.createWorkspacePersistentState<boolean>( + 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<boolean> { + 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<boolean> { + 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<NodeJS.ProcessEnv | undefined>; + /** + * Ensures that the dependencies required for TensorBoard are installed in Active Environment for the given resource. + */ + ensureDependenciesAreInstalled(resource?: Uri): Promise<boolean>; + /** + * 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<TensorboardExtensionApi> | 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<boolean> => + this.dependencyChcker.ensureDependenciesAreInstalled(resource), + isPromptEnabled: () => this.tensorBoardPrompt.isPromptEnabled(), + }); + return undefined; + } + + public async integrateWithTensorboardExtension(): Promise<void> { + const api = await this.getExtensionApi(); + if (api) { + this.registerApi(api); + } + } + + private async getExtensionApi(): Promise<TensorboardExtensionApi | undefined> { + if (!this.tensorboardExtension) { + const extension = this.extensions.getExtension<TensorboardExtensionApi>(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 131840c83489..ed26916e3eaa 100644 --- a/src/client/terminals/activation.ts +++ b/src/client/terminals/activation.ts @@ -4,25 +4,67 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { Disposable, Terminal } from 'vscode'; -import { ITerminalManager } from '../common/application/types'; +import { Terminal, Uri } from 'vscode'; +import { IActiveResourceService, ITerminalManager } from '../common/application/types'; import { ITerminalActivator } from '../common/terminal/types'; -import { IDisposableRegistry } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; +import { IDisposable, IDisposableRegistry } from '../common/types'; import { ITerminalAutoActivation } from './types'; +import { shouldEnvExtHandleActivation } from '../envExt/api.internal'; @injectable() export class TerminalAutoActivation implements ITerminalAutoActivation { - constructor(@inject(IServiceContainer) private container: IServiceContainer, - @inject(ITerminalActivator) private readonly activator: ITerminalActivator) { + private handler?: IDisposable; + + private readonly terminalsNotToAutoActivate = new WeakSet<Terminal>(); + + constructor( + @inject(ITerminalManager) + private readonly terminalManager: ITerminalManager, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(ITerminalActivator) private readonly activator: ITerminalActivator, + @inject(IActiveResourceService) + private readonly activeResourceService: IActiveResourceService, + ) { + disposableRegistry.push(this); + } + + public dispose(): void { + if (this.handler) { + this.handler.dispose(); + this.handler = undefined; + } + } + + public register(): void { + if (this.handler) { + return; + } + this.handler = this.terminalManager.onDidOpenTerminal(this.activateTerminal, this); } - public register() { - const manager = this.container.get<ITerminalManager>(ITerminalManager); - const disposables = this.container.get<Disposable[]>(IDisposableRegistry); - const disposable = manager.onDidOpenTerminal(this.activateTerminal, this); - disposables.push(disposable); + + public disableAutoActivation(terminal: Terminal): void { + this.terminalsNotToAutoActivate.add(terminal); } + private async activateTerminal(terminal: Terminal): Promise<void> { - await this.activator.activateEnvironmentInTerminal(terminal, undefined); + if (this.terminalsNotToAutoActivate.has(terminal)) { + return; + } + if (shouldEnvExtHandleActivation()) { + return; + } + if ('hideFromUser' in terminal.creationOptions && terminal.creationOptions.hideFromUser) { + return; + } + + 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, + }); } } diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index 04ccf5e61c19..48165adcd169 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -4,50 +4,192 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { Disposable, Uri } from 'vscode'; +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 { IDisposableRegistry } from '../../common/types'; +import '../../common/extensions'; +import { IDisposableRegistry, IConfigurationService, Resource } from '../../common/types'; +import { noop } from '../../common/utils/misc'; +import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { captureTelemetry } from '../../telemetry'; -import { EXECUTION_CODE, EXECUTION_DJANGO } from '../../telemetry/constants'; +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 { - constructor(@inject(ICommandManager) private commandManager: ICommandManager, + private eventEmitter: EventEmitter<string> = new EventEmitter<string>(); + constructor( + @inject(ICommandManager) private commandManager: ICommandManager, @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(IDisposableRegistry) private disposableRegistry: Disposable[], - @inject(IServiceContainer) private serviceContainer: IServiceContainer) { + @inject(IConfigurationService) private readonly configSettings: IConfigurationService, + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + ) {} + public registerCommands() { + [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'; + 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>(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 as any, async (file: Resource) => { + const interpreterService = this.serviceContainer.get<IInterpreterService>(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 as any, + async (file: Resource) => { + const interpreterService = this.serviceContainer.get<IInterpreterService>(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'); + }); + }, + ), + ); } - public registerCommands() { - this.disposableRegistry.push(this.commandManager.registerCommand(Commands.Exec_In_Terminal, this.executeFileInterTerminal.bind(this))); - this.disposableRegistry.push(this.commandManager.registerCommand(Commands.Exec_Selection_In_Terminal, this.executeSelectionInTerminal.bind(this))); - this.disposableRegistry.push(this.commandManager.registerCommand(Commands.Exec_Selection_In_Django_Shell, this.executeSelectionInDjangoShell.bind(this))); + private async executeUsingExtension(file: Resource, dedicated: boolean): Promise<void> { + const codeExecutionHelper = this.serviceContainer.get<ICodeExecutionHelper>(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; + } + + // 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(); + } } - @captureTelemetry(EXECUTION_CODE, { scope: 'file' }, false) - private async executeFileInterTerminal(file?: Uri) { + + private async executeFileInTerminal( + file: Resource, + trigger: 'command' | 'icon', + options?: { newTerminalPerFile: boolean }, + ): Promise<void> { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { + scope: 'file', + trigger, + newTerminalPerFile: options?.newTerminalPerFile, + }); const codeExecutionHelper = this.serviceContainer.get<ICodeExecutionHelper>(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); + const fileAfterSave = await codeExecutionHelper.saveFileIfDirty(fileToExecute); + if (fileAfterSave) { + fileToExecute = fileAfterSave; + } + const executionService = this.serviceContainer.get<ICodeExecutionService>(ICodeExecutionService, 'standard'); - await executionService.executeFile(fileToExecute); + await executionService.executeFile(fileToExecute, options); } - @captureTelemetry(EXECUTION_CODE, { scope: 'selection' }, false) + @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) private async executeSelectionInTerminal(): Promise<void> { const executionService = this.serviceContainer.get<ICodeExecutionService>(ICodeExecutionService, 'standard'); await this.executeSelection(executionService); } - @captureTelemetry(EXECUTION_DJANGO, { scope: 'selection' }, false) + @captureTelemetry(EventName.EXECUTION_DJANGO, { scope: 'selection' }, false) private async executeSelectionInDjangoShell(): Promise<void> { const executionService = this.serviceContainer.get<ICodeExecutionService>(ICodeExecutionService, 'djangoShell'); await this.executeSelection(executionService); @@ -59,12 +201,32 @@ export class CodeExecutionManager implements ICodeExecutionManager { return; } const codeExecutionHelper = this.serviceContainer.get<ICodeExecutionHelper>(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; } - await executionService.execute(normalizedCode, activeEditor!.document.uri); + try { + this.eventEmitter.fire(normalizedCode); + } catch { + // Ignore any errors that occur for firing this event. It's only used + // for telemetry + noop(); + } + + await executionService.execute(normalizedCode, activeEditor.document.uri); + } + + 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 d6f8755d447b..74643084db28 100644 --- a/src/client/terminals/codeExecution/djangoContext.ts +++ b/src/client/terminals/codeExecution/djangoContext.ts @@ -7,28 +7,31 @@ import { Disposable } from 'vscode'; import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types'; import { ContextKey } from '../../common/contextKey'; import { IFileSystem } from '../../common/platform/types'; +import { traceError } from '../../logging'; @injectable() export class DjangoContextInitializer implements Disposable { private readonly isDjangoProject: ContextKey; - private monitoringActiveTextEditor: boolean; + private monitoringActiveTextEditor: boolean = false; private workspaceContextKeyValues = new Map<string, boolean>(); - private lastCheckedWorkspace: string; + private lastCheckedWorkspace: string = ''; private disposables: Disposable[] = []; - constructor(private documentManager: IDocumentManager, + constructor( + private documentManager: IDocumentManager, private workpaceService: IWorkspaceService, private fileSystem: IFileSystem, - commandManager: ICommandManager) { - + commandManager: ICommandManager, + ) { this.isDjangoProject = new ContextKey('python.isDjangoProject', commandManager); - this.ensureContextStateIsSet() - .catch(ex => console.error('Python Extension: ensureState', ex)); - this.disposables.push(this.workpaceService.onDidChangeWorkspaceFolders(() => this.updateContextKeyBasedOnActiveWorkspace())); + this.ensureContextStateIsSet().catch((ex) => traceError('Python Extension: ensureState', ex)); + this.disposables.push( + this.workpaceService.onDidChangeWorkspaceFolders(() => this.updateContextKeyBasedOnActiveWorkspace()), + ); } public dispose() { - this.disposables.forEach(disposable => disposable.dispose()); + this.disposables.forEach((disposable) => disposable.dispose()); } private updateContextKeyBasedOnActiveWorkspace() { if (this.monitoringActiveTextEditor) { @@ -38,7 +41,10 @@ export class DjangoContextInitializer implements Disposable { this.disposables.push(this.documentManager.onDidChangeActiveTextEditor(() => this.ensureContextStateIsSet())); } private getActiveWorkspace(): string | undefined { - if (!Array.isArray(this.workpaceService.workspaceFolders) || this.workpaceService.workspaceFolders.length === 0) { + if ( + !Array.isArray(this.workpaceService.workspaceFolders) || + this.workpaceService.workspaceFolders.length === 0 + ) { return; } if (this.workpaceService.workspaceFolders.length === 1) { diff --git a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts index 3066ec27fb71..05a1470b5727 100644 --- a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts +++ b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts @@ -6,41 +6,66 @@ 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'; @injectable() export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvider { - constructor(@inject(ITerminalServiceFactory) terminalServiceFactory: ITerminalServiceFactory, + constructor( + @inject(ITerminalServiceFactory) terminalServiceFactory: ITerminalServiceFactory, @inject(IConfigurationService) configurationService: IConfigurationService, @inject(IWorkspaceService) workspace: IWorkspaceService, @inject(IDocumentManager) documentManager: IDocumentManager, @inject(IPlatformService) platformService: IPlatformService, @inject(ICommandManager) commandManager: ICommandManager, @inject(IFileSystem) fileSystem: IFileSystem, - @inject(IDisposableRegistry) disposableRegistry: Disposable[]) { - - super(terminalServiceFactory, configurationService, workspace, disposableRegistry, platformService); + @inject(IDisposableRegistry) disposableRegistry: Disposable[], + @inject(IInterpreterService) interpreterService: IInterpreterService, + @inject(IApplicationShell) applicationShell: IApplicationShell, + ) { + super( + terminalServiceFactory, + configurationService, + workspace, + disposableRegistry, + platformService, + interpreterService, + commandManager, + applicationShell, + ); this.terminalTitle = 'Django Shell'; disposableRegistry.push(new DjangoContextInitializer(documentManager, workspace, fileSystem, commandManager)); } - public getReplCommandArgs(resource?: Uri): { command: string; args: string[] } { - const pythonSettings = this.configurationService.getSettings(resource); - const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath; - const args = pythonSettings.terminal.launchArgs.slice(); + + public async getExecutableInfo(resource?: Uri, args: string[] = []): Promise<PythonExecInfo> { + const info = await super.getExecutableInfo(resource, args); const workspaceUri = resource ? this.workspace.getWorkspaceFolder(resource) : undefined; - const defaultWorkspace = Array.isArray(this.workspace.workspaceFolders) && this.workspace.workspaceFolders.length > 0 ? this.workspace.workspaceFolders[0].uri.fsPath : ''; + const defaultWorkspace = + Array.isArray(this.workspace.workspaceFolders) && this.workspace.workspaceFolders.length > 0 + ? this.workspace.workspaceFolders[0].uri.fsPath + : ''; const workspaceRoot = workspaceUri ? workspaceUri.uri.fsPath : defaultWorkspace; const managePyPath = workspaceRoot.length === 0 ? 'manage.py' : path.join(workspaceRoot, 'manage.py'); - args.push(managePyPath.fileToCommandArgument()); - args.push('shell'); - return { command, args }; + return copyPythonExecInfo(info, [managePyPath.fileToCommandArgumentForPythonExt(), 'shell']); + } + + public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise<PythonExecInfo> { + // We need the executable info but not the 'manage.py shell' args + const info = await super.getExecutableInfo(resource); + return copyPythonExecInfo(info, executeArgs); } } diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index 17204ba0dcea..4efad5ee174e 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -1,85 +1,325 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import '../../common/extensions'; import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Range, TextEditor, Uri } from 'vscode'; -import { IApplicationShell, IDocumentManager } from '../../common/application/types'; -import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../common/constants'; -import '../../common/extensions'; +import { l10n, Position, Range, TextEditor, Uri } from 'vscode'; + +import { + IActiveResourceService, + IApplicationShell, + ICommandManager, + IDocumentManager, + IWorkspaceService, +} from '../../common/application/types'; +import { PYTHON_LANGUAGE } from '../../common/constants'; +import * as internalScripts from '../../common/process/internal/scripts'; import { IProcessServiceFactory } from '../../common/process/types'; -import { IConfigurationService } from '../../common/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 configurationService: IConfigurationService; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + + private readonly interpreterService: IInterpreterService; + + 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>(IDocumentManager); this.applicationShell = serviceContainer.get<IApplicationShell>(IApplicationShell); this.processServiceFactory = serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory); - this.configurationService = serviceContainer.get<IConfigurationService>(IConfigurationService); + this.interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService); + this.configSettings = serviceContainer.get<IConfigurationService>(IConfigurationService); + this.commandManager = serviceContainer.get<ICommandManager>(ICommandManager); + this.activeResourceService = this.serviceContainer.get<IActiveResourceService>(IActiveResourceService); } - public async normalizeLines(code: string, resource?: Uri): Promise<string> { + + public async normalizeLines( + code: string, + _replType: ReplType, + wholeFileContent?: string, + resource?: Uri, + ): Promise<string> { try { if (code.trim().length === 0) { return ''; } - const pythonPath = this.configurationService.getSettings(resource).pythonPath; - const args = [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'normalizeForInterpreter.py'), code]; + // 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 proc = await processService.exec(pythonPath, args, { throwOnStdErr: true }); - return proc.stdout; + const [args, parse] = internalScripts.normalizeSelection(); + const observable = processService.execObservable(interpreter?.path || 'python', args, { + throwOnStdErr: true, + }); + const normalizeOutput = createDeferred<string>(); + + // 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>(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) { - console.error(ex, 'Python: Failed to normalize code for execution in terminal'); + 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<void> { + 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<Uri | undefined> { - 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<string | undefined> { 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<void> { - const docs = this.documentManager.textDocuments.filter(d => d.uri.path === file.path); - if (docs.length === 1 && docs[0].isDirty) { - await docs[0].save(); + + public async saveFileIfDirty(file: Uri): Promise<Resource> { + const docs = this.documentManager.textDocuments.filter((d) => d.uri.path === file.path); + if (docs.length === 1 && (docs[0].isDirty || docs[0].isUntitled)) { + const workspaceService = this.serviceContainer.get<IWorkspaceService>(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 f745745213b0..bc9a30af1fac 100644 --- a/src/client/terminals/codeExecution/repl.ts +++ b/src/client/terminals/codeExecution/repl.ts @@ -5,22 +5,35 @@ 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 } from '../../common/types'; -import { IDisposableRegistry } from '../../common/types'; +import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { IInterpreterService } from '../../interpreter/contracts'; import { TerminalCodeExecutionProvider } from './terminalCodeExecution'; @injectable() export class ReplProvider extends TerminalCodeExecutionProvider { - constructor( @inject(ITerminalServiceFactory) terminalServiceFactory: ITerminalServiceFactory, + constructor( + @inject(ITerminalServiceFactory) terminalServiceFactory: ITerminalServiceFactory, @inject(IConfigurationService) configurationService: IConfigurationService, @inject(IWorkspaceService) workspace: IWorkspaceService, @inject(IDisposableRegistry) disposableRegistry: Disposable[], - @inject(IPlatformService) platformService: IPlatformService) { - - super(terminalServiceFactory, configurationService, workspace, disposableRegistry, platformService); + @inject(IPlatformService) platformService: IPlatformService, + @inject(IInterpreterService) interpreterService: IInterpreterService, + @inject(ICommandManager) commandManager: ICommandManager, + @inject(IApplicationShell) applicationShell: IApplicationShell, + ) { + 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 7e541ee12126..ea444af4d89e 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -6,83 +6,152 @@ 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<boolean>; - constructor(@inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, + + 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) { - const pythonSettings = this.configurationService.getSettings(file); + 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.setCwdForFileExecution(file); - - const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath; - const launchArgs = pythonSettings.terminal.launchArgs; - - await this.getTerminalService(file).sendCommand(command, launchArgs.concat(file.fsPath.fileToCommandArgument())); + await this.getTerminalService(file, options).sendCommand(command, args); } public async execute(code: string, resource?: Uri): Promise<void> { 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; } - this.replActive = new Promise<boolean>(async resolve => { - const replCommandArgs = this.getReplCommandArgs(resource); - await this.getTerminalService(resource).sendCommand(replCommandArgs.command, replCommandArgs.args); + sendTelemetryEvent(EventName.REPL, undefined, { replType: 'Terminal' }); + this.replActive = new Promise<boolean>(async (resolve) => { + const replCommandArgs = await this.getExecutableInfo(resource); + let listener: IDisposable; + Promise.race([ + new Promise<boolean>((resolve) => setTimeout(() => resolve(true), 3000)), + new Promise<boolean>((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 getReplCommandArgs(resource?: Uri): { command: string; args: string[] } { + + public async getExecutableInfo(resource?: Uri, args: string[] = []): Promise<PythonExecInfo> { const pythonSettings = this.configurationService.getSettings(resource); - const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath; - const args = pythonSettings.terminal.launchArgs.slice(); - return { command, args }; + 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]); } - 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; + + // Overridden in subclasses, see djangoShellCodeExecution.ts + public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise<PythonExecInfo> { + return this.getExecutableInfo(resource, executeArgs); + } + 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<void> { + 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<string | undefined> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<number | undefined, boolean>(); + + // 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<EnvironmentVariableMutatorOptions> { + 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<TerminalShellType>(); + + private readonly didChange = new EventEmitter<void>(); + + 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<boolean>(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<boolean>('integrated.shellIntegration.enabled'); + if (!isEnabled) { + traceVerbose('Shell integration is disabled in user settings.'); + } + } + + public readonly onDidChangeStatus = this.didChange.event; + + public async isWorking(): Promise<boolean> { + 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<boolean> { + const shellType = identifyShellFromShellPath(shell); + const isSupposedToWork = ShellIntegrationShells.includes(shellType); + if (!isSupposedToWork) { + return false; + } + const key = getKeyForShell(shellType); + const persistedResult = this.persistentStateFactory.createGlobalPersistentState<boolean>(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<void> { + const config = getConfiguration('python'); + const pythonrcSetting = config.get<boolean>('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<void> { + 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<CustomTerminalLink> { + provideTerminalLinks( + context: TerminalLinkContext, + _token: CancellationToken, + ): ProviderResult<CustomTerminalLink[]> { + 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<void> { + 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 e6625a7784df..e62701dcec0e 100644 --- a/src/client/terminals/serviceRegistry.ts +++ b/src/client/terminals/serviceRegistry.ts @@ -8,13 +8,52 @@ import { DjangoShellCodeExecutionProvider } from './codeExecution/djangoShellCod 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'; -export function registerTypes(serviceManager: IServiceManager) { +export function registerTypes(serviceManager: IServiceManager): void { serviceManager.addSingleton<ICodeExecutionHelper>(ICodeExecutionHelper, CodeExecutionHelper); + serviceManager.addSingleton<ICodeExecutionManager>(ICodeExecutionManager, CodeExecutionManager); - serviceManager.addSingleton<ICodeExecutionService>(ICodeExecutionService, DjangoShellCodeExecutionProvider, 'djangoShell'); - serviceManager.addSingleton<ICodeExecutionService>(ICodeExecutionService, TerminalCodeExecutionProvider, 'standard'); + + serviceManager.addSingleton<ICodeExecutionService>( + ICodeExecutionService, + DjangoShellCodeExecutionProvider, + 'djangoShell', + ); + serviceManager.addSingleton<ICodeExecutionService>( + ICodeExecutionService, + TerminalCodeExecutionProvider, + 'standard', + ); serviceManager.addSingleton<ICodeExecutionService>(ICodeExecutionService, ReplProvider, 'repl'); + serviceManager.addSingleton<ITerminalAutoActivation>(ITerminalAutoActivation, TerminalAutoActivation); + serviceManager.addSingleton<ITerminalEnvVarCollectionService>( + ITerminalEnvVarCollectionService, + TerminalEnvVarCollectionService, + ); + serviceManager.addSingleton<ITerminalDeactivateService>(ITerminalDeactivateService, TerminalDeactivateService); + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + TerminalIndicatorPrompt, + ); + serviceManager.addSingleton<IShellIntegrationDetectionService>( + IShellIntegrationDetectionService, + ShellIntegrationDetectionService, + ); + + serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); } diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index d47d4b612e51..1384057c3b7c 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -1,22 +1,24 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { TextEditor, Uri } from 'vscode'; +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<void>; - executeFile(file: Uri): Promise<void>; + executeFile(file: Uri, options?: { newTerminalPerFile: boolean }): Promise<void>; initializeRepl(resource?: Uri): Promise<void>; } export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper'); export interface ICodeExecutionHelper { - normalizeLines(code: string): Promise<string>; + normalizeLines(code: string, replType: ReplType, wholeFileContent?: string, resource?: Uri): Promise<string>; getFileToExecute(): Promise<Uri | undefined>; - saveFileIfDirty(file: Uri): Promise<void>; + saveFileIfDirty(file: Uri): Promise<Resource>; getSelectedTextToExecute(textEditor: TextEditor): Promise<string | undefined>; } @@ -27,6 +29,32 @@ export interface ICodeExecutionManager { } export const ITerminalAutoActivation = Symbol('ITerminalAutoActivation'); -export interface 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<void>; + isWorking(): Promise<boolean>; +} + +export const ITerminalDeactivateService = Symbol('ITerminalDeactivateService'); +export interface ITerminalDeactivateService { + initializeScriptParams(shell: string): Promise<void>; + getScriptLocation(shell: string, resource: Resource): Promise<string | undefined>; +} + +export const IPythonStartupEnvVarService = Symbol('IPythonStartupEnvVarService'); +export interface IPythonStartupEnvVarService { register(): void; } 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<void> { + this.ops.push(['updateTestArgs', testDirectory, product, args]); + return Promise.resolve(); + } + + public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise<void> { + this.ops.push(['enable', testDirectory, product, []]); + return Promise.resolve(); + } + + public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise<void> { + this.ops.push(['disable', testDirectory, product, []]); + return Promise.resolve(); + } + + public async apply(cfg: ITestConfigSettingsService): Promise<void> { + 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>(IWorkspaceService); + } + + public async updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]): Promise<void> { + const setting = this.getTestArgSetting(product); + return this.updateSetting(testDirectory, setting, args); + } + + public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise<void> { + const setting = this.getTestEnablingSetting(product); + return this.updateSetting(testDirectory, setting, true); + } + + public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise<void> { + 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 new file mode 100644 index 000000000000..4f41e60c8806 --- /dev/null +++ b/src/client/testing/common/constants.ts @@ -0,0 +1,7 @@ +import { Product } from '../../common/types'; +import { TestProvider } from '../types'; +import { UnitTestProduct } from './types'; + +export const UNIT_TEST_PRODUCTS: UnitTestProduct[] = [Product.pytest, Product.unittest]; +export const PYTEST_PROVIDER: TestProvider = 'pytest'; +export const UNITTEST_PROVIDER: TestProvider = 'unittest'; diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts new file mode 100644 index 000000000000..037bfb265088 --- /dev/null +++ b/src/client/testing/common/debugLauncher.ts @@ -0,0 +1,365 @@ +import { inject, injectable, named } from 'inversify'; +import * as path from 'path'; +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 * as internalScripts from '../../common/process/internal/scripts'; +import { IConfigurationService, IPythonSettings } from '../../common/types'; +import { DebuggerTypeName, PythonDebuggerTypeName } from '../../debugger/constants'; +import { IDebugConfigurationResolver } from '../../debugger/extension/configuration/types'; +import { DebugPurpose, LaunchRequestArguments } from '../../debugger/types'; +import { IServiceContainer } from '../../ioc/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; + + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IDebugConfigurationResolver) + @named('launch') + private readonly launchResolver: IDebugConfigurationResolver<LaunchRequestArguments>, + ) { + this.configService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); + } + + /** + * 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<void> { + const deferred = createDeferred<void>(); + 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 = DebugLauncher.resolveWorkspaceFolder(options.cwd); + const launchArgs = await this.getLaunchArgs( + options, + workspaceFolder, + this.configService.getSettings(workspaceFolder.uri), + ); + const debugManager = this.serviceContainer.get<IDebugService>(IDebugService); + + // 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 { + 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 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 = getWorkspaceFolder(cwdUri); + if (!workspaceFolder) { + const [first] = getWorkspaceFolders()!; + workspaceFolder = first; + } + return workspaceFolder; + } + + private async getLaunchArgs( + options: LaunchOptions, + workspaceFolder: WorkspaceFolder, + configSettings: IPythonSettings, + ): Promise<LaunchRequestArguments> { + let debugConfig = await DebugLauncher.readDebugConfig(workspaceFolder); + if (!debugConfig) { + debugConfig = { + name: 'Debug Unit Test', + type: 'debugpy', + request: 'test', + 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, 'python_files'), + include: false, + }); + + DebugLauncher.applyDefaults(debugConfig!, workspaceFolder, configSettings, options.cwd); + + return this.convertConfigToArgs(debugConfig!, workspaceFolder, options); + } + + public async readAllDebugConfigs(workspace: WorkspaceFolder): Promise<DebugConfiguration[]> { + try { + const configs = await getConfigurationsForWorkspace(workspace); + return configs; + } catch (exc) { + traceError('could not get debug config', exc); + const appShell = this.serviceContainer.get<IApplicationShell>(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<LaunchRequestArguments | undefined> { + 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 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; + } + } + + private static applyDefaults( + cfg: LaunchRequestArguments, + workspaceFolder: WorkspaceFolder, + configSettings: IPythonSettings, + optionsCwd?: string, + ) { + // cfg.pythonPath is handled by LaunchConfigurationResolver. + + if (!cfg.console) { + cfg.console = 'internalConsole'; + } + if (!cfg.cwd) { + // 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 = {}; + } + if (!cfg.envFile) { + cfg.envFile = configSettings.envFile; + } + if (cfg.stopOnEntry === undefined) { + cfg.stopOnEntry = false; + } + cfg.showReturnValue = cfg.showReturnValue !== false; + if (cfg.redirectOutput === undefined) { + cfg.redirectOutput = true; + } + if (cfg.debugStdLib === undefined) { + cfg.debugStdLib = false; + } + if (cfg.subProcess === undefined) { + cfg.subProcess = true; + } + } + + private async convertConfigToArgs( + debugConfig: LaunchRequestArguments, + workspaceFolder: WorkspaceFolder, + options: LaunchOptions, + ): Promise<LaunchRequestArguments> { + const configArgs = debugConfig as LaunchRequestArguments; + const testArgs = + options.testProvider === 'unittest' ? options.args.filter((item) => item !== '--debug') : options.args; + const script = DebugLauncher.getTestLauncherScript(options.testProvider); + const args = script(testArgs); + const [program] = args; + configArgs.program = program; + + configArgs.args = args.slice(1); + // We leave configArgs.request as "test" so it will be sent in telemetry. + + let launchArgs = await this.launchResolver.resolveDebugConfiguration( + workspaceFolder, + configArgs, + 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'; + + if (options.pytestPort && options.runTestIdsPort) { + launchArgs.env = { + ...launchArgs.env, + TEST_RUN_PIPE: options.pytestPort, + RUN_TEST_IDS_PIPE: options.runTestIdsPort, + }; + } else { + 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 static getTestLauncherScript(testProvider: TestProvider) { + switch (testProvider) { + case 'unittest': { + return internalScripts.execution_py_testlauncher; // this is the new way to run unittest execution, debugger + } + 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/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/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>(IInstaller); + this.testConfigSettingsService = + cfg || serviceContainer.get<ITestConfigSettingsService>(ITestConfigSettingsService); + } + + public abstract configure(wkspace: Uri): Promise<void>; + + public abstract requiresUserToConfigure(wkspace: Uri): Promise<boolean>; + + public async enable(): Promise<void> { + // 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<void> { + return this.testConfigSettingsService.enable(this.workspace, this.product); + } + + protected selectTestDir(rootDir: string, subDirs: string[], customOptions: QuickPickItem[] = []): Promise<string> { + 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<string> { + 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<string[]> { + const fs = this.serviceContainer.get<IFileSystem>(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<string> { + const def = createDeferred<string>(); + const appShell = this.serviceContainer.get<IApplicationShell>(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 new file mode 100644 index 000000000000..04e82e1caa52 --- /dev/null +++ b/src/client/testing/common/testUtils.ts @@ -0,0 +1,65 @@ +import { injectable } from 'inversify'; +import { Uri, workspace } from 'vscode'; +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<Uri | undefined> { + if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { + return undefined; + } else if (workspace.workspaceFolders.length === 1) { + return workspace.workspaceFolders[0].uri; + } else { + const workspaceFolder = await appShell.showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); + return workspaceFolder ? workspaceFolder.uri : undefined; + } +} + +@injectable() +export class TestsHelper implements ITestsHelper { + public parseProviderName(product: UnitTestProduct): TestProvider { + switch (product) { + case Product.pytest: + return 'pytest'; + case Product.unittest: + return 'unittest'; + default: { + throw new Error(`Unknown Test Product ${product}`); + } + } + } + public parseProduct(provider: TestProvider): UnitTestProduct { + switch (provider) { + case 'pytest': + return Product.pytest; + case 'unittest': + return Product.unittest; + default: { + throw new Error(`Unknown Test Provider ${provider}`); + } + } + } + public getSettingsPropertyNames(product: UnitTestProduct): TestSettingsPropertyNames { + const id = this.parseProviderName(product); + switch (id) { + case 'pytest': { + return { + argsName: 'pytestArgs' as keyof ITestingSettings, + pathName: 'pytestPath' as keyof ITestingSettings, + enabledName: 'pytestEnabled' as keyof ITestingSettings, + }; + } + case 'unittest': { + return { + argsName: 'unittestArgs' as keyof ITestingSettings, + enabledName: 'unittestEnabled' as keyof ITestingSettings, + }; + } + default: { + throw new Error(`Unknown Test Provider '${product}'`); + } + } + } +} diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts new file mode 100644 index 000000000000..e2fa2d6d2e5a --- /dev/null +++ b/src/client/testing/common/types.ts @@ -0,0 +1,83 @@ +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 UnitTestProduct = Product.pytest | Product.unittest; + +// **************** +// test args/options + +export type TestDiscoveryOptions = { + workspaceFolder: Uri; + cwd: string; + args: string[]; + token?: CancellationToken; + ignoreCache: boolean; + outChannel?: OutputChannel; +}; + +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 enum TestFilter { + removeTests = 'removeTests', + discovery = 'discovery', + runAll = 'runAll', + runSpecific = 'runSpecific', + debugAll = 'debugAll', + debugSpecific = 'debugSpecific', +} + +// **************** +// interfaces + +export const ITestsHelper = Symbol('ITestsHelper'); +export interface ITestsHelper { + parseProviderName(product: UnitTestProduct): TestProvider; + parseProduct(provider: TestProvider): UnitTestProduct; + getSettingsPropertyNames(product: Product): TestSettingsPropertyNames; +} + +export const ITestConfigurationService = Symbol('ITestConfigurationService'); +export interface ITestConfigurationService { + hasConfiguredTests(wkspace: Uri): boolean; + selectTestRunner(placeHolderMessage: string): Promise<UnitTestProduct | undefined>; + enableTest(wkspace: Uri, product: UnitTestProduct): Promise<void>; + promptToEnableAndConfigureTestFramework(wkspace: Uri): Promise<void>; +} + +export const ITestConfigSettingsService = Symbol('ITestConfigSettingsService'); +export interface ITestConfigSettingsService { + updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]): Promise<void>; + enable(testDirectory: string | Uri, product: UnitTestProduct): Promise<void>; + disable(testDirectory: string | Uri, product: UnitTestProduct): Promise<void>; + getTestEnablingSetting(product: UnitTestProduct): string; +} + +export interface ITestConfigurationManager { + requiresUserToConfigure(wkspace: Uri): Promise<boolean>; + configure(wkspace: Uri): Promise<void>; + enable(): Promise<void>; + disable(): Promise<void>; +} + +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, callback?: () => void, sessionOptions?: DebugSessionOptions): Promise<void>; +} 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>(IConfigurationService); + this.appShell = serviceContainer.get<IApplicationShell>(IApplicationShell); + this.workspaceService = serviceContainer.get<IWorkspaceService>(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<UnitTestProduct | undefined> { + 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<void> { + const factory = this.serviceContainer.get<ITestConfigurationManagerFactory>(ITestConfigurationManagerFactory); + const configMgr = factory.create(wkspace, product); + return this._enableTest(wkspace, configMgr); + } + + public async promptToEnableAndConfigureTestFramework(wkspace: Uri): Promise<void> { + await this._promptToEnableAndConfigureTestFramework(wkspace, undefined, false, 'commandpalette'); + } + + private _enableTest(wkspace: Uri, configMgr: ITestConfigurationManager) { + const pythonConfig = this.workspaceService.getConfiguration('python', wkspace); + if (pythonConfig.get<boolean>('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<void> { + 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>(ITestsHelper); + telemetryProps.tool = helper.parseProviderName(selectedTestRunner); + const delayed = new BufferedTestConfigSettingsService(); + const factory = this.serviceContainer.get<ITestConfigurationManagerFactory>( + 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>(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>(IApplicationShell); + this.pytestInstallationHelper = new PytestInstallationHelper(appShell); + } + + public async requiresUserToConfigure(wkspace: Uri): Promise<boolean> { + 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<void> { + 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<string[]> { + const fs = this.serviceContainer.get<IFileSystem>(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<boolean> { + 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<boolean> { + 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<boolean> { + 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<boolean> { + return true; + } + + public async configure(wkspace: Uri): Promise<void> { + 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 new file mode 100644 index 000000000000..b661a38d5779 --- /dev/null +++ b/src/client/testing/configurationFactory.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { Product } from '../common/types'; +import { IServiceContainer } from '../ioc/types'; +import * as 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 { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + public create(wkspace: Uri, product: Product, cfg?: ITestConfigSettingsService): ITestConfigurationManager { + switch (product) { + case Product.unittest: { + return new unittest.ConfigurationManager(wkspace, this.serviceContainer, cfg); + } + case Product.pytest: { + return new pytest.ConfigurationManager(wkspace, this.serviceContainer, cfg); + } + default: { + throw new Error('Invalid test configuration'); + } + } + } +} diff --git a/src/client/testing/main.ts b/src/client/testing/main.ts new file mode 100644 index 000000000000..eed4d70e852c --- /dev/null +++ b/src/client/testing/main.ts @@ -0,0 +1,230 @@ +'use strict'; + +import { inject, injectable } from 'inversify'; +import { + ConfigurationChangeEvent, + Disposable, + Uri, + tests, + TestResultState, + WorkspaceFolder, + Command, + TestItem, +} from 'vscode'; +import { IApplicationShell, ICommandManager, IContextKeyManager, IWorkspaceService } from '../common/application/types'; +import * as constants from '../common/constants'; +import '../common/extensions'; +import { IDisposableRegistry, Product } from '../common/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { EventName } from '../telemetry/constants'; +import { sendTelemetryEvent } from '../telemetry/index'; +import { selectTestWorkspace } from './common/testUtils'; +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 TestingService implements ITestingService { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + + public getSettingsPropertyNames(product: Product): TestSettingsPropertyNames { + const helper = this.serviceContainer.get<ITestsHelper>(ITestsHelper); + return helper.getSettingsPropertyNames(product); + } +} + +/** + * 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<Disposable[]>(IDisposableRegistry); + const commandManager = serviceContainer.get<ICommandManager>(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>(IWorkspaceService); + + let wkspace: Uri | undefined; + if (resource) { + const wkspaceFolder = workspaceService.getWorkspaceFolder(resource); + wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; + } else { + const appShell = serviceContainer.get<IApplicationShell>(IApplicationShell); + wkspace = await selectTestWorkspace(appShell); + } + if (!wkspace) { + return; + } + const interpreterService = serviceContainer.get<IInterpreterService>(IInterpreterService); + const cmdManager = serviceContainer.get<ICommandManager>(ICommandManager); + if (!(await interpreterService.getActiveInterpreter(wkspace))) { + cmdManager.executeCommand(constants.Commands.TriggerEnvironmentSelection, wkspace); + return; + } + const configurationService = serviceContainer.get<ITestConfigurationService>(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>(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>(IWorkspaceService); + const wkspaceFolder = + workspaceService.getWorkspaceFolder(resource) || workspaceService.workspaceFolders?.at(0); + if (!wkspaceFolder) { + return undefined; + } + + const configurationService = serviceContainer.get<ITestConfigurationService>(ITestConfigurationService); + if (configurationService.hasConfiguredTests(wkspaceFolder.uri)) { + return undefined; + } + + 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); + }), + ); +} + +@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; + + // This is temporarily needed until the proposed API settles for this part + private testStateMap: Map<string, TestResultState> = new Map(); + + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.disposableRegistry = serviceContainer.get<Disposable[]>(IDisposableRegistry); + this.workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService); + this.context = this.serviceContainer.get<IContextKeyManager>(IContextKeyManager); + + if (tests && !!tests.createTestController) { + this.testController = serviceContainer.get<ITestController>(ITestController); + } + + const configChangeTrigger = new DelayedTrigger( + this.configurationChangeHandler.bind(this), + 500, + 'Test Configuration Change', + ); + this.configChangeTrigger = configChangeTrigger; + this.disposableRegistry.push(configChangeTrigger); + } + + public async activate(): Promise<void> { + if (this.activatedOnce) { + return; + } + this.activatedOnce = true; + + this.registerHandlers(); + + if (!!tests.testResults) { + await this.updateTestUIButtons(); + this.disposableRegistry.push( + tests.onDidChangeTestResults(() => { + this.updateTestUIButtons(); + }), + ); + } + + 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>(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.', + ); + } + }); + } + } + + 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); + + const hasFailedTests = checkForFailedTests(this.testStateMap); + await this.context.setContext(ExtensionContextKey.HasFailedTests, hasFailedTests); + } + + 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); + + await Promise.all(changedWorkspaces.map((u) => this.testController?.refreshTestData(u))); + } + + private registerHandlers() { + const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService); + this.disposableRegistry.push( + this.workspaceService.onDidChangeConfiguration((e) => { + 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/serviceRegistry.ts b/src/client/testing/serviceRegistry.ts new file mode 100644 index 000000000000..d36fab7686f8 --- /dev/null +++ b/src/client/testing/serviceRegistry.ts @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IExtensionActivationService } from '../activation/types'; +import { IServiceManager } from '../ioc/types'; +import { DebugLauncher } from './common/debugLauncher'; +import { TestConfigSettingsService } from './common/configSettingService'; +import { TestsHelper } from './common/testUtils'; +import { + ITestConfigSettingsService, + ITestConfigurationManagerFactory, + ITestConfigurationService, + ITestDebugLauncher, + ITestsHelper, +} from './common/types'; +import { UnitTestConfigurationService } from './configuration'; +import { TestConfigurationManagerFactory } from './configurationFactory'; +import { TestingService, UnitTestManagementService } from './main'; +import { ITestingService } from './types'; +import { registerTestControllerTypes } from './testController/serviceRegistry'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton<ITestDebugLauncher>(ITestDebugLauncher, DebugLauncher); + + serviceManager.add<ITestsHelper>(ITestsHelper, TestsHelper); + + serviceManager.addSingleton<ITestConfigurationService>(ITestConfigurationService, UnitTestConfigurationService); + serviceManager.addSingleton<ITestingService>(ITestingService, TestingService); + + serviceManager.addSingleton<ITestConfigSettingsService>(ITestConfigSettingsService, TestConfigSettingsService); + serviceManager.addSingleton<ITestConfigurationManagerFactory>( + ITestConfigurationManagerFactory, + TestConfigurationManagerFactory, + ); + serviceManager.addSingleton<IExtensionActivationService>(IExtensionActivationService, UnitTestManagementService); + + 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<void>, + 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<void>, + 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<void> { + 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<string, ProjectAdapter | undefined>; + projectPathToAdapter: Map<string, ProjectAdapter>; +} + +/** Groups test items by owning project using env API or path-based matching as fallback. */ +export async function groupTestItemsByProject( + testItems: TestItem[], + projects: ProjectAdapter[], +): Promise<Map<string, { project: ProjectAdapter; items: TestItem[] }>> { + const result = new Map<string, { project: ProjectAdapter; items: TestItem[] }>(); + + // 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<ProjectAdapter | undefined> { + 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<void> { + const processedTestItemIds = new Set<string>(); + const uniqueTestCaseIds = new Set<string>(); + + // 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<FileCoverageDetail[]> => { + 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<string, PythonProject>. + * + * @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<string, FileCoverageDetail[]>(); + + /** + * 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<string, TestItem> { + return this.testItemIndex.runIdToTestItemMap; + } + + public get runIdToVSid(): Map<string, string> { + return this.testItemIndex.runIdToVSidMap; + } + + public get vsIdToRunId(): Map<string, string> { + 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<string, FileCoverageDetail[]> { + const detailedCoverageMap = new Map<string, FileCoverageDetail[]>(); + + 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<string, TestItem>; + private runIdToVSid: Map<string, string>; + private vsIdToRunId: Map<string, string>; + private subtestStatsMap: Map<string, SubtestStats>; + + constructor() { + this.runIdToTestItem = new Map<string, TestItem>(); + this.runIdToVSid = new Map<string, string>(); + this.vsIdToRunId = new Map<string, string>(); + this.subtestStatsMap = new Map<string, SubtestStats>(); + } + + /** + * 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<string, TestItem> { + return this.runIdToTestItem; + } + + public get runIdToVSidMap(): Map<string, string> { + return this.runIdToVSid; + } + + public get vsIdToRunIdMap(): Map<string, string> { + 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<string, TestData>, + 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<string, TestData>, + 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<string, TestData>, + 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<string, TestData>, + 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<string, TestData>, + 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<string, TestData>, + 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<string, TestData>, + 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<string, TestData>, + 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<string, TestData>, + 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<string, TestData>, + testRoot: string, + rawDataSet: RawDiscoveredTests[], + token?: CancellationToken, +): Promise<void> { + 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<string, TestData>, + testRoot: string, + rawDataSet: RawDiscoveredTests[], + token?: CancellationToken, +): Promise<void> { + 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<string, TestData>): 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<string, TestResultState>, 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<string, TestResultState>, + 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<string, TestResultState>): 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<string, PythonProject>. + */ +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<Uri, Map<string, ProjectAdapter>> = 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<string, ProjectAdapter> | 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<ProjectAdapter[]> { + 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<string, ProjectAdapter>(); + 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<ProjectAdapter[]> { + 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<ProjectAdapter> { + 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<ProjectAdapter> { + 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<string, string[]> { + const ignoreMap = new Map<string, string[]>(); + 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<void>; + stopRefreshing(): void; + onRefreshingCompleted: Event<void>; + onRefreshingStarted: Event<void>; + onRunWithoutConfiguration: Event<WorkspaceFolder[]>; +} + +export const ITestFrameworkController = Symbol('ITestFrameworkController'); +export interface ITestFrameworkController { + resolveChildren(testController: TestController, item: TestItem, token?: CancellationToken): Promise<void>; +} + +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<DataReceivedEvent>; +// readonly onRunDataReceived: Event<DataReceivedEvent>; +// readonly onDiscoveryDataReceived: Event<DataReceivedEvent>; +// sendCommand( +// options: TestCommandOptions, +// env: EnvironmentVariables, +// runTestIdsPort?: string, +// runInstance?: TestRun, +// testIds?: string[], +// callback?: () => void, +// executionFactory?: IPythonExecutionFactory, +// ): Promise<void>; +// serverReady(): Promise<void>; +// 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<string, string>; + runIdToTestItem: Map<string, TestItem>; + vsIdToRunId: Map<string, string>; +} + +export interface ITestResultResolver extends ITestItemMappings { + detailedCoverageMap: Map<string, FileCoverageDetail[]>; + + 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<void>; +} + +// 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<void>; +} + +// 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<void> { + return createDeferred<void>(); +} + +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<string> { + // temp file name in format of test-ids-<randomSuffix>.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<void>, + cancellationToken?: CancellationToken, +): Promise<string> { + 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<string> { + 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<boolean>} - Returns true if any parent directory is a symlink, otherwise false. + */ +export async function hasSymlinkParent(currentPath: string): Promise<boolean> { + 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<Uri, WorkspaceTestAdapter> = 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<void> = new EventEmitter<void>(); + + private readonly refreshingStartedEvent: EventEmitter<void> = new EventEmitter<void>(); + + private readonly runWithoutConfigurationEvent: EventEmitter<WorkspaceFolder[]> = 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<void> { + 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<void> { + 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<void> { + 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<WorkspaceFolder>(); + + 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<void> { + 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<void> { + 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<void> { + 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<void> { + // 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<string>(); + + // 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<string>): Promise<void> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<FileCoverageDetail[]> => { + 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<void> { + 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<string, RawDiscoveredTests[]> = new Map(); + + private discovering: Map<string, Deferred<void>> = new Map(); + + private idToRawData: Map<string, TestData> = new Map(); + + constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) {} + + public async resolveChildren( + testController: TestController, + item: TestItem, + token?: CancellationToken, + ): Promise<void> { + 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<NodeJS.ProcessEnv> { + 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<void> { + // 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<void> = 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<void> { + const deferredTillServerClose: Deferred<void> = 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<ExecutionTestPayload> { + 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<void> = 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<void> = 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<string[]> { + 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>(ITestFrameworkController, PytestController, PYTEST_PROVIDER); + + serviceManager.addSingleton<ITestFrameworkController>( + ITestFrameworkController, + UnittestController, + UNITTEST_PROVIDER, + ); + serviceManager.addSingleton<ITestController>(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<NodeJS.ProcessEnv> { + 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<void> { + // 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<void> { + // deferredTillServerClose awaits named pipe server close + const deferredTillServerClose: Deferred<void> = 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<ExecutionTestPayload> { + 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<ExecutionResult<string>>(); + + 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<string, RawDiscoveredTests> = new Map(); + + private discovering: Map<string, Deferred<void>> = new Map(); + + private idToRawData: Map<string, TestData> = new Map(); + + constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) {} + + public async resolveChildren( + testController: TestController, + item: TestItem, + token?: CancellationToken, + ): Promise<void> { + 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<void> | undefined; + + private executing: Deferred<void> | 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<void> { + if (this.executing) { + traceError('Test execution already in progress, not starting a new one.'); + return this.executing.promise; + } + + const deferred = createDeferred<void>(); + this.executing = deferred; + + const testCaseNodes: TestItem[] = []; + const testCaseIdsSet = new Set<string>(); + 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<void> { + 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<void>(); + 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 new file mode 100644 index 000000000000..da308ee6998b --- /dev/null +++ b/src/client/testing/types.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Product } from '../common/types'; +import { TestSettingsPropertyNames } from './configuration/types'; + +export type TestProvider = 'pytest' | 'unittest'; + +// **************** +// interfaces + +export const ITestingService = Symbol('ITestingService'); +export interface ITestingService { + getSettingsPropertyNames(product: Product): TestSettingsPropertyNames; +} 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<void> { + 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<void> { + return env.clipboard.writeText(text); +} diff --git a/src/client/typeFormatters/blockFormatProvider.ts b/src/client/typeFormatters/blockFormatProvider.ts deleted file mode 100644 index 415d8692e7d1..000000000000 --- a/src/client/typeFormatters/blockFormatProvider.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { CancellationToken, FormattingOptions, OnTypeFormattingEditProvider, TextDocument, TextEdit } from 'vscode'; -import { Position } from 'vscode'; -import { CodeBlockFormatProvider } from './codeBlockFormatProvider'; -import { ASYNC_FOR_IN_REGEX, ELIF_REGEX, ELSE_REGEX, FOR_IN_REGEX, IF_REGEX, WHILE_REGEX } from './contracts'; -import { EXCEPT_REGEX, FINALLY_REGEX, TRY_REGEX } from './contracts'; -import { ASYNC_DEF_REGEX, CLASS_REGEX, DEF_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 d7518ad8b419..000000000000 --- a/src/client/typeFormatters/codeBlockFormatProvider.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { FormattingOptions, TextDocument, TextEdit } from 'vscode'; -import { Position, Range, 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 efd376b75b69..000000000000 --- a/src/client/typeFormatters/contracts.ts +++ /dev/null @@ -1,23 +0,0 @@ -export class BlockRegEx { - constructor(private regEx: RegExp, public startWord) { - - } - 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 86c22bc60d47..000000000000 --- a/src/client/typeFormatters/dispatcher.ts +++ /dev/null @@ -1,40 +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: { [key: string]: OnTypeFormattingEditProvider }; - - constructor(providers: { [key: string]: OnTypeFormattingEditProvider }) { - this.providers = providers; - } - - public provideOnTypeFormattingEdits(document: TextDocument, position: Position, ch: string, options: FormattingOptions, cancellationToken: CancellationToken): ProviderResult<TextEdit[]> { - 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 3e17e714d6ee..000000000000 --- a/src/client/typeFormatters/onEnterFormatter.ts +++ /dev/null @@ -1,39 +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/unittests/codeLenses/main.ts b/src/client/unittests/codeLenses/main.ts deleted file mode 100644 index a21db7abd877..000000000000 --- a/src/client/unittests/codeLenses/main.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as vscode from 'vscode'; -import { PYTHON } from '../../common/constants'; -import { ITestCollectionStorageService } from '../common/types'; -import { TestFileCodeLensProvider } from './testFiles'; - -export function activateCodeLenses(onDidChange: vscode.EventEmitter<void>, - symboldProvider: vscode.DocumentSymbolProvider, testCollectionStorage: ITestCollectionStorageService): vscode.Disposable { - - const disposables: vscode.Disposable[] = []; - const codeLensProvider = new TestFileCodeLensProvider(onDidChange, symboldProvider, testCollectionStorage); - disposables.push(vscode.languages.registerCodeLensProvider(PYTHON, codeLensProvider)); - - return { - dispose: () => { disposables.forEach(d => d.dispose()); } - }; -} diff --git a/src/client/unittests/codeLenses/testFiles.ts b/src/client/unittests/codeLenses/testFiles.ts deleted file mode 100644 index 29086c6850fb..000000000000 --- a/src/client/unittests/codeLenses/testFiles.ts +++ /dev/null @@ -1,261 +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, workspace } from 'vscode'; -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 { - // tslint:disable-next-line:variable-name - constructor(private _onDidChange: EventEmitter<void>, - private symbolProvider: DocumentSymbolProvider, - private testCollectionStorage: ITestCollectionStorageService) { - } - - get onDidChangeCodeLenses(): Event<void> { - return this._onDidChange.event; - } - - public async provideCodeLenses(document: TextDocument, token: CancellationToken) { - const wkspace = workspace.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> { - codeLens.command = { command: 'python.runtests', title: 'Test' }; - return Promise.resolve(codeLens); - } - - private async getCodeLenses(document: TextDocument, token: CancellationToken, symbolProvider: DocumentSymbolProvider) { - const wkspace = workspace.getWorkspaceFolder(document.uri); - if (!wkspace) { - return []; - } - const tests = this.testCollectionStorage.getTests(wkspace.uri); - if (!tests) { - return []; - } - const file = tests.testFiles.find(item => item.fullPath === document.uri.fsPath); - 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, <TestsToRun>{ testSuite: [cls] }] - }), - new CodeLens(range, { - title: getTestStatusIcon(cls.status) + constants.Text.CodeLensDebugUnitTest, - command: constants.Commands.Tests_Debug, - arguments: [undefined, CommandSource.codelens, file, <TestsToRun>{ testSuite: [cls] }] - }) - ]; - } - default: { - return []; - } - } - } -} - -function getTestStatusIcon(status?: TestStatus): string { - switch (status) { - case TestStatus.Pass: { - return '✔ '; - } - case TestStatus.Error: - case TestStatus.Fail: { - return '✘ '; - } - case TestStatus.Skipped: { - return '⃠ '; - } - 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(`✔ ${count}`); - } - count = fns.filter(fn => fn.status === TestStatus.Error || fn.status === TestStatus.Fail).length; - if (count > 0) { - statuses.push(`✘ ${count}`); - } - count = fns.filter(fn => fn.status === TestStatus.Skipped).length; - if (count > 0) { - statuses.push(`⃠ ${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, <TestsToRun>{ testFunction: [fn] }] - }), - new CodeLens(range, { - title: getTestStatusIcon(fn.status) + constants.Text.CodeLensDebugUnitTest, - command: constants.Commands.Tests_Debug, - arguments: [undefined, CommandSource.codelens, file, <TestsToRun>{ 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 []; - } - if (functions.length === 0) { - return [ - new CodeLens(range, { - title: constants.Text.CodeLensRunUnitTest, - command: constants.Commands.Tests_Run, - arguments: [undefined, CommandSource.codelens, file, <TestsToRun>{ testFunction: functions }] - }), - new CodeLens(range, { - title: constants.Text.CodeLensDebugUnitTest, - command: constants.Commands.Tests_Debug, - arguments: [undefined, CommandSource.codelens, file, <TestsToRun>{ testFunction: functions }] - }) - ]; - } - - // 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/unittests/common/argumentsHelper.ts b/src/client/unittests/common/argumentsHelper.ts deleted file mode 100644 index 3842a9a6874e..000000000000 --- a/src/client/unittests/common/argumentsHelper.ts +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { ILogger } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { IArgumentsHelper } from '../types'; - -@injectable() -export class ArgumentsHelper implements IArgumentsHelper { - private readonly logger: ILogger; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.logger = serviceContainer.get<ILogger>(ILogger); - } - 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[] { - let lastIndexOfOption = -1; - args.forEach((arg, index) => { - if (optionsWithoutArguments.indexOf(arg) !== -1) { - lastIndexOfOption = index; - return; - } else if (optionsWithArguments.indexOf(arg) !== -1) { - // Cuz the next item is the value. - lastIndexOfOption = index + 1; - } else if (optionsWithArguments.findIndex(item => arg.startsWith(`${item}=`)) !== -1) { - lastIndexOfOption = index; - return; - } else if (arg.startsWith('-')) { - // Ok this is an unknown option, lets treat this as one without values. - this.logger.logWarning(`Unknown command line option passed into args parser for tests '${arg}'. Please report on https://github.com/Microsoft/vscode-python/issues/new`); - lastIndexOfOption = index; - return; - } else if (args.indexOf('=') > 0) { - // Ok this is an unknown option with a value - this.logger.logWarning(`Unknown command line option passed into args parser for tests '${arg}'. Please report on https://github.com/Microsoft/vscode-python/issues/new`); - lastIndexOfOption = index; - } - }); - return args.slice(lastIndexOfOption + 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/unittests/common/constants.ts b/src/client/unittests/common/constants.ts deleted file mode 100644 index e361f4479bcf..000000000000 --- a/src/client/unittests/common/constants.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { TestProvider } from './types'; - -export const CANCELLATION_REASON = 'cancelled_user_request'; -export enum CommandSource { - auto = 'auto', - ui = 'ui', - codelens = 'codelens', - commandPalette = 'commandpalette' -} -export const TEST_OUTPUT_CHANNEL = 'TEST_OUTPUT_CHANNEL'; -export const NOSETEST_PROVIDER: TestProvider = 'nosetest'; -export const PYTEST_PROVIDER: TestProvider = 'pytest'; -export const UNITTEST_PROVIDER: TestProvider = 'unittest'; diff --git a/src/client/unittests/common/debugLauncher.ts b/src/client/unittests/common/debugLauncher.ts deleted file mode 100644 index 86ad1e2fa5b4..000000000000 --- a/src/client/unittests/common/debugLauncher.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { IDebugService, IWorkspaceService } from '../../common/application/types'; -import { EXTENSION_ROOT_DIR } from '../../common/constants'; -import { IConfigurationService } from '../../common/types'; -import { DebugOptions } from '../../debugger/types'; -import { IServiceContainer } from '../../ioc/types'; -import { ITestDebugLauncher, LaunchOptions, TestProvider } from './types'; - -@injectable() -export class DebugLauncher implements ITestDebugLauncher { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } - public async launchDebugger(options: LaunchOptions) { - if (options.token && options.token!.isCancellationRequested) { - return; - } - const cwdUri = options.cwd ? Uri.file(options.cwd) : undefined; - const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - if (!workspaceService.hasWorkspaceFolders) { - throw new Error('Please open a workspace'); - } - let workspaceFolder = workspaceService.getWorkspaceFolder(cwdUri!); - if (!workspaceFolder) { - workspaceFolder = workspaceService.workspaceFolders![0]; - } - - const cwd = cwdUri ? cwdUri.fsPath : workspaceFolder.uri.fsPath; - const configSettings = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(Uri.file(cwd)); - const debugManager = this.serviceContainer.get<IDebugService>(IDebugService); - const debugArgs = this.fixArgs(options.args, options.testProvider); - const program = this.getTestLauncherScript(options.testProvider); - return debugManager.startDebugging(workspaceFolder, { - name: 'Debug Unit Test', - type: 'python', - request: 'launch', - program, - cwd, - args: debugArgs, - console: 'none', - envFile: configSettings.envFile, - debugOptions: [DebugOptions.RedirectOutput] - }).then(() => void (0)); - } - private fixArgs(args: string[], testProvider: TestProvider): string[] { - if (testProvider === 'unittest') { - return args.filter(item => item !== '--debug'); - } else { - return args; - } - } - private getTestLauncherScript(testProvider: TestProvider) { - switch (testProvider) { - case 'unittest': { - return 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}'`); - } - } - } -} diff --git a/src/client/unittests/common/managers/baseTestManager.ts b/src/client/unittests/common/managers/baseTestManager.ts deleted file mode 100644 index 215881c8554e..000000000000 --- a/src/client/unittests/common/managers/baseTestManager.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { CancellationToken, CancellationTokenSource, Diagnostic, DiagnosticCollection, DiagnosticRelatedInformation, Disposable, languages, OutputChannel, Uri } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import { isNotInstalledError } from '../../../common/helpers'; -import { IFileSystem } from '../../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry, IInstaller, IOutputChannel, IPythonSettings, Product } from '../../../common/types'; -import { getNamesAndValues } from '../../../common/utils/enum'; -import { IServiceContainer } from '../../../ioc/types'; -import { UNITTEST_DISCOVER, UNITTEST_RUN } from '../../../telemetry/constants'; -import { sendTelemetryEvent } from '../../../telemetry/index'; -import { TestDiscoverytTelemetry, TestRunTelemetry } from '../../../telemetry/types'; -import { IPythonUnitTestMessage, IUnitTestDiagnosticService } from '../../types'; -import { CANCELLATION_REASON, CommandSource, TEST_OUTPUT_CHANNEL } from './../constants'; -import { ITestCollectionStorageService, ITestDiscoveryService, ITestManager, ITestResultsService, ITestsHelper, TestDiscoveryOptions, TestProvider, Tests, TestStatus, TestsToRun } from './../types'; - -enum CancellationTokenType { - testDiscovery, - testRunner -} - -export abstract class BaseTestManager implements ITestManager { - public diagnosticCollection: DiagnosticCollection; - protected readonly settings: IPythonSettings; - private readonly unitTestDiagnosticService: IUnitTestDiagnosticService; - public abstract get enabled(): boolean; - protected get outputChannel() { - return this._outputChannel; - } - protected get testResultsService() { - return this._testResultsService; - } - private testCollectionStorage: ITestCollectionStorageService; - private _testResultsService: ITestResultsService; - private workspaceService: IWorkspaceService; - private _outputChannel: OutputChannel; - private tests?: Tests; - private _status: TestStatus = TestStatus.Unknown; - private testDiscoveryCancellationTokenSource?: CancellationTokenSource; - private testRunnerCancellationTokenSource?: CancellationTokenSource; - private _installer!: IInstaller; - private discoverTestsPromise?: Promise<Tests>; - private get installer(): IInstaller { - if (!this._installer) { - this._installer = this.serviceContainer.get<IInstaller>(IInstaller); - } - return this._installer; - } - constructor(public readonly testProvider: TestProvider, private product: Product, public readonly workspaceFolder: Uri, protected rootDirectory: string, - protected serviceContainer: IServiceContainer) { - this._status = TestStatus.Unknown; - const configService = serviceContainer.get<IConfigurationService>(IConfigurationService); - this.settings = configService.getSettings(this.rootDirectory ? Uri.file(this.rootDirectory) : undefined); - const disposables = serviceContainer.get<Disposable[]>(IDisposableRegistry); - this._outputChannel = this.serviceContainer.get<OutputChannel>(IOutputChannel, TEST_OUTPUT_CHANNEL); - this.testCollectionStorage = this.serviceContainer.get<ITestCollectionStorageService>(ITestCollectionStorageService); - this._testResultsService = this.serviceContainer.get<ITestResultsService>(ITestResultsService); - this.workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - this.diagnosticCollection = languages.createDiagnosticCollection(this.testProvider); - this.unitTestDiagnosticService = serviceContainer.get<IUnitTestDiagnosticService>(IUnitTestDiagnosticService); - 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 workingDirectory(): string { - return this.settings.unitTest.cwd && this.settings.unitTest.cwd.length > 0 ? this.settings.unitTest.cwd : this.rootDirectory; - } - public stop() { - if (this.testDiscoveryCancellationTokenSource) { - this.testDiscoveryCancellationTokenSource.cancel(); - } - if (this.testRunnerCancellationTokenSource) { - this.testRunnerCancellationTokenSource.cancel(); - } - } - public reset() { - this._status = TestStatus.Unknown; - this.tests = undefined; - } - 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): Promise<Tests> { - if (this.discoverTestsPromise) { - return this.discoverTestsPromise!; - } - - if (!ignoreCache && this.tests! && this.tests!.testFunctions.length > 0) { - this._status = TestStatus.Idle; - return Promise.resolve(this.tests!); - } - this._status = 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.createCancellationToken(CancellationTokenType.testDiscovery); - const discoveryOptions = this.getDiscoveryOptions(ignoreCache); - const discoveryService = this.serviceContainer.get<ITestDiscoveryService>(ITestDiscoveryService, this.testProvider); - return discoveryService.discoverTests(discoveryOptions) - .then(tests => { - this.tests = tests; - this._status = TestStatus.Idle; - this.resetTestResults(); - 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>(ITestsHelper); - testsHelper.displayTestErrorMessage('There were some errors in discovering unit tests'); - } - const wkspace = this.workspaceService.getWorkspaceFolder(Uri.file(this.rootDirectory))!.uri; - this.testCollectionStorage.storeTests(wkspace, tests); - this.disposeCancellationToken(CancellationTokenType.testDiscovery); - sendTelemetryEvent(UNITTEST_DISCOVER, undefined, telementryProperties); - return tests; - }).catch((reason: {}) => { - if (isNotInstalledError(reason as Error) && !quietMode) { - this.installer.promptToInstall(this.product, this.workspaceFolder) - .catch(ex => console.error('Python Extension: isNotInstalledError', ex)); - } - - this.tests = undefined; - this.discoverTestsPromise = undefined; - if (this.testDiscoveryCancellationToken && this.testDiscoveryCancellationToken.isCancellationRequested) { - reason = CANCELLATION_REASON; - this._status = TestStatus.Idle; - } else { - telementryProperties.failed = true; - sendTelemetryEvent(UNITTEST_DISCOVER, undefined, telementryProperties); - this._status = TestStatus.Error; - this.outputChannel.appendLine('Test Discovery failed: '); - // tslint:disable-next-line:prefer-template - this.outputChannel.appendLine(reason.toString()); - } - const wkspace = this.workspaceService.getWorkspaceFolder(Uri.file(this.rootDirectory))!.uri; - this.testCollectionStorage.storeTests(wkspace, null); - this.disposeCancellationToken(CancellationTokenType.testDiscovery); - return Promise.reject(reason); - }); - } - public runTest(cmdSource: CommandSource, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<Tests> { - 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>(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 === true) { - // tslint:disable-next-line:prefer-template - moreInfo.Run_Failed_Tests = runFailedTests.toString(); - telementryProperties.scope = 'failed'; - } - 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'; - } - } - - if (runFailedTests === false && testsToRun === null) { - this.resetTestResults(); - } - - this._status = TestStatus.Running; - if (this.testRunnerCancellationTokenSource) { - this.testRunnerCancellationTokenSource.cancel(); - } - // 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<Tests>(reason); - } - const testsHelper = this.serviceContainer.get<ITestsHelper>(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.createCancellationToken(CancellationTokenType.testRunner); - return this.runTestImpl(tests, testsToRun, runFailedTests, debug); - }).then(() => { - this._status = TestStatus.Idle; - this.disposeCancellationToken(CancellationTokenType.testRunner); - sendTelemetryEvent(UNITTEST_RUN, undefined, telementryProperties); - return this.tests!; - }).catch(reason => { - if (this.testRunnerCancellationToken && this.testRunnerCancellationToken.isCancellationRequested) { - reason = CANCELLATION_REASON; - this._status = TestStatus.Idle; - } else { - this._status = TestStatus.Error; - telementryProperties.failed = true; - sendTelemetryEvent(UNITTEST_RUN, undefined, telementryProperties); - } - this.disposeCancellationToken(CancellationTokenType.testRunner); - return Promise.reject<Tests>(reason); - }); - } - public async updateDiagnostics(tests: Tests, messages: IPythonUnitTestMessage[]): Promise<void> { - 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>(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); - } - } - // tslint:disable-next-line:no-any - protected abstract runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<any>; - protected abstract getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions; - 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: IPythonUnitTestMessage[]): Promise<void> { - 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: IPythonUnitTestMessage): Diagnostic { - const stackStart = message.locationStack[0]; - const diagPrefix = this.unitTestDiagnosticService.getMessagePrefix(message.status); - const severity = this.unitTestDiagnosticService.getSeverity(message.severity)!; - const diagMsg = 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/unittests/common/managers/testConfigurationManager.ts b/src/client/unittests/common/managers/testConfigurationManager.ts deleted file mode 100644 index 3e3008788133..000000000000 --- a/src/client/unittests/common/managers/testConfigurationManager.ts +++ /dev/null @@ -1,111 +0,0 @@ -import * as path from 'path'; -import { OutputChannel, QuickPickItem, Uri, window } from 'vscode'; -import { IInstaller, IOutputChannel, Product } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; -import { getSubDirectories } from '../../../common/utils/fs'; -import { IServiceContainer } from '../../../ioc/types'; -import { ITestConfigurationManager } from '../../types'; -import { TEST_OUTPUT_CHANNEL } from '../constants'; -import { ITestConfigSettingsService, 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) { - this.outputChannel = serviceContainer.get<OutputChannel>(IOutputChannel, TEST_OUTPUT_CHANNEL); - this.installer = serviceContainer.get<IInstaller>(IInstaller); - this.testConfigSettingsService = serviceContainer.get<ITestConfigSettingsService>(ITestConfigSettingsService); - } - public abstract configure(wkspace: Uri): Promise<void>; - public abstract requiresUserToConfigure(wkspace: Uri): Promise<boolean>; - public async enable() { - // Disable other test frameworks. - const testProducsToDisable = [Product.pytest, Product.unittest, Product.nosetest] - .filter(item => item !== this.product) as UnitTestProduct[]; - - for (const prod of testProducsToDisable) { - await this.testConfigSettingsService.disable(this.workspace, prod); - } - - return 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<string> { - const options = { - matchOnDescription: true, - matchOnDetail: true, - placeHolder: 'Select the directory containing the unit 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<string>(); - window.showQuickPick(items, options).then(item => { - if (!item) { - return def.resolve(); - } - - def.resolve(item.label); - }); - - return def.promise; - } - - protected selectTestFilePattern(): Promise<string> { - const options = { - 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 begining with \'test\'' }, - { label: 'test_*.py', description: 'Python Files begining with \'test_\'' }, - { label: '*test*.py', description: 'Python Files containing the word \'test\'' } - ]; - - const def = createDeferred<string>(); - window.showQuickPick(items, options).then(item => { - if (!item) { - return def.resolve(); - } - - def.resolve(item.label); - }); - - return def.promise; - } - protected getTestDirs(rootDir: string): Promise<string[]> { - return 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; - }); - } -} diff --git a/src/client/unittests/common/runner.ts b/src/client/unittests/common/runner.ts deleted file mode 100644 index 3345ff72996d..000000000000 --- a/src/client/unittests/common/runner.ts +++ /dev/null @@ -1,113 +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<string> { - return run(this.serviceContainer, testProvider, options); - } -} - -export async function run(serviceContainer: IServiceContainer, testProvider: TestProvider, options: Options): Promise<string> { - const testExecutablePath = getExecutablePath(testProvider, serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(options.workspaceFolder)); - const moduleName = getTestModuleName(testProvider); - const spawnOptions = options as SpawnOptions; - let pythonExecutionServicePromise: Promise<IPythonExecutionService>; - spawnOptions.mergeStdOutErr = typeof spawnOptions.mergeStdOutErr === 'boolean' ? spawnOptions.mergeStdOutErr : true; - - let promise: Promise<ObservableExecutionResult<string>>; - - if (!testExecutablePath && testProvider === UNITTEST_PROVIDER) { - // Unit tests have a special way of being executed - const pythonServiceFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); - pythonExecutionServicePromise = pythonServiceFactory.create({ resource: options.workspaceFolder }); - promise = pythonExecutionServicePromise.then(executionService => executionService.execObservable(options.args, { ...spawnOptions })); - } else { - const pythonToolsExecutionService = serviceContainer.get<IPythonToolExecutionService>(IPythonToolExecutionService); - const testHelper = serviceContainer.get<ITestsHelper>(ITestsHelper); - const executionInfo: ExecutionInfo = { - execPath: testExecutablePath, - args: options.args, - moduleName: testExecutablePath && testExecutablePath.length > 0 ? undefined : moduleName, - product: testHelper.parseProduct(testProvider) - }; - promise = pythonToolsExecutionService.execObservable(executionInfo, spawnOptions, options.workspaceFolder); - } - - return promise.then(result => { - return new Promise<string>((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.unitTest.nosetestPath; - break; - } - case PYTEST_PROVIDER: { - testRunnerExecutablePath = settings.unitTest.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/unittests/common/services/configSettingService.ts b/src/client/unittests/common/services/configSettingService.ts deleted file mode 100644 index 483ffa35058b..000000000000 --- a/src/client/unittests/common/services/configSettingService.ts +++ /dev/null @@ -1,71 +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, UnitTestProduct } from './../types'; - -@injectable() -export class TestConfigSettingsService implements ITestConfigSettingsService { - private readonly workspaceService: IWorkspaceService; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.workspaceService = serviceContainer.get<IWorkspaceService>(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<void> { - const setting = this.getTestEnablingSetting(product); - return this.updateSetting(testDirectory, setting, true); - } - - public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise<void> { - const setting = this.getTestEnablingSetting(product); - return this.updateSetting(testDirectory, setting, false); - } - private getTestArgSetting(product: UnitTestProduct) { - switch (product) { - case Product.unittest: - return 'unitTest.unittestArgs'; - case Product.pytest: - return 'unitTest.pyTestArgs'; - case Product.nosetest: - return 'unitTest.nosetestArgs'; - default: - throw new Error('Invalid Test Product'); - } - } - private getTestEnablingSetting(product: UnitTestProduct) { - switch (product) { - case Product.unittest: - return 'unitTest.unittestEnabled'; - case Product.pytest: - return 'unitTest.pyTestEnabled'; - case Product.nosetest: - return 'unitTest.nosetestsEnabled'; - 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); - } -} diff --git a/src/client/unittests/common/services/storageService.ts b/src/client/unittests/common/services/storageService.ts deleted file mode 100644 index 859cf2939cbf..000000000000 --- a/src/client/unittests/common/services/storageService.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { inject, injectable } from 'inversify'; -import { Disposable, Uri, workspace } from 'vscode'; -import { IDisposableRegistry } from '../../../common/types'; -import { ITestCollectionStorageService, Tests } from './../types'; - -@injectable() -export class TestCollectionStorageService implements ITestCollectionStorageService { - private testsIndexedByWorkspaceUri = new Map<string, Tests | undefined>(); - constructor(@inject(IDisposableRegistry) disposables: Disposable[]) { - disposables.push(this); - } - public getTests(wkspace: Uri): Tests | undefined { - const workspaceFolder = this.getWorkspaceFolderPath(wkspace) || ''; - return this.testsIndexedByWorkspaceUri.has(workspaceFolder) ? this.testsIndexedByWorkspaceUri.get(workspaceFolder) : undefined; - } - public storeTests(wkspace: Uri, tests: Tests | undefined): void { - const workspaceFolder = this.getWorkspaceFolderPath(wkspace) || ''; - this.testsIndexedByWorkspaceUri.set(workspaceFolder, tests); - } - public dispose() { - this.testsIndexedByWorkspaceUri.clear(); - } - private getWorkspaceFolderPath(resource: Uri): string | undefined { - const folder = workspace.getWorkspaceFolder(resource); - return folder ? folder.uri.path : undefined; - } -} diff --git a/src/client/unittests/common/services/testManagerService.ts b/src/client/unittests/common/services/testManagerService.ts deleted file mode 100644 index 2e19904fae38..000000000000 --- a/src/client/unittests/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<Product, ITestManager>(); - private readonly configurationService: IConfigurationService; - constructor(private wkspace: Uri, private testsHelper: ITestsHelper, private serviceContainer: IServiceContainer) { - const disposables = serviceContainer.get<Disposable[]>(IDisposableRegistry); - this.configurationService = serviceContainer.get<IConfigurationService>(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>(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.unitTest.cwd && settings.unitTest.cwd.length > 0 ? settings.unitTest.cwd : this.wkspace.fsPath; - } - public getPreferredTestManager(): UnitTestProduct | undefined { - const settings = this.configurationService.getSettings(this.wkspace); - if (settings.unitTest.nosetestsEnabled) { - return Product.nosetest; - } else if (settings.unitTest.pyTestEnabled) { - return Product.pytest; - } else if (settings.unitTest.unittestEnabled) { - return Product.unittest; - } - return undefined; - } -} diff --git a/src/client/unittests/common/services/testResultsService.ts b/src/client/unittests/common/services/testResultsService.ts deleted file mode 100644 index b041a397b967..000000000000 --- a/src/client/unittests/common/services/testResultsService.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { inject, injectable, named } from 'inversify'; -import { ITestResultsService, ITestVisitor, TestFile, TestFolder, Tests, TestStatus, TestSuite } 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 { - tests.testFiles.forEach(test => this.updateTestFileResults(test)); - tests.testFolders.forEach(folder => this.updateTestFolderResults(folder)); - } - private updateTestSuiteResults(test: TestSuite): void { - this.updateTestSuiteAndFileResults(test); - } - private updateTestFileResults(test: TestFile): void { - this.updateTestSuiteAndFileResults(test); - } - private updateTestFolderResults(testFolder: TestFolder): void { - let allFilesPassed = true; - let allFilesRan = true; - - testFolder.testFiles.forEach(fl => { - if (allFilesPassed && typeof fl.passed === 'boolean') { - if (!fl.passed) { - allFilesPassed = false; - } - } else { - allFilesRan = false; - } - - testFolder.functionsFailed! += fl.functionsFailed!; - testFolder.functionsPassed! += fl.functionsPassed!; - }); - - let allFoldersPassed = true; - let allFoldersRan = true; - - testFolder.folders.forEach(folder => { - this.updateTestFolderResults(folder); - if (allFoldersPassed && typeof folder.passed === 'boolean') { - if (!folder.passed) { - allFoldersPassed = false; - } - } else { - allFoldersRan = false; - } - - testFolder.functionsFailed! += folder.functionsFailed!; - testFolder.functionsPassed! += folder.functionsPassed!; - }); - - if (allFilesRan && allFoldersRan) { - testFolder.passed = allFilesPassed && allFoldersPassed; - testFolder.status = testFolder.passed ? TestStatus.Idle : TestStatus.Fail; - } else { - testFolder.passed = undefined; - testFolder.status = TestStatus.Unknown; - } - } - private updateTestSuiteAndFileResults(test: TestSuite | TestFile): void { - let totalTime = 0; - let allFunctionsPassed = true; - let allFunctionsRan = true; - - test.functions.forEach(fn => { - totalTime += fn.time; - if (typeof fn.passed === 'boolean') { - if (fn.passed) { - test.functionsPassed! += 1; - } else { - test.functionsFailed! += 1; - allFunctionsPassed = false; - } - } else { - allFunctionsRan = false; - } - }); - - let allSuitesPassed = true; - let allSuitesRan = true; - - test.suites.forEach(suite => { - this.updateTestSuiteResults(suite); - totalTime += suite.time; - if (allSuitesRan && typeof suite.passed === 'boolean') { - if (!suite.passed) { - allSuitesPassed = false; - } - } else { - allSuitesRan = false; - } - - test.functionsFailed! += suite.functionsFailed!; - test.functionsPassed! += suite.functionsPassed!; - }); - - test.time = totalTime; - if (allSuitesRan && allFunctionsRan) { - test.passed = allFunctionsPassed && allSuitesPassed; - test.status = test.passed ? TestStatus.Idle : TestStatus.Error; - } else { - test.passed = undefined; - test.status = TestStatus.Unknown; - } - } -} diff --git a/src/client/unittests/common/services/unitTestDiagnosticService.ts b/src/client/unittests/common/services/unitTestDiagnosticService.ts deleted file mode 100644 index d83775d7d150..000000000000 --- a/src/client/unittests/common/services/unitTestDiagnosticService.ts +++ /dev/null @@ -1,38 +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, IUnitTestDiagnosticService, PythonUnitTestMessageSeverity } from '../../types'; -import { TestStatus } from '../types'; - -@injectable() -export class UnitTestDiagnosticService implements IUnitTestDiagnosticService { - private MessageTypes = new Map<TestStatus, DiagnosticMessageType>(); - private MessageSeverities = new Map<PythonUnitTestMessageSeverity, DiagnosticSeverity>(); - private MessagePrefixes = new Map<DiagnosticMessageType, string>(); - - 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(PythonUnitTestMessageSeverity.Error, DiagnosticSeverity.Error); - this.MessageSeverities.set(PythonUnitTestMessageSeverity.Failure, DiagnosticSeverity.Error); - this.MessageSeverities.set(PythonUnitTestMessageSeverity.Skip, DiagnosticSeverity.Information); - this.MessageSeverities.set(PythonUnitTestMessageSeverity.Pass, null); - this.MessagePrefixes.set(DiagnosticMessageType.Error, localize.UnitTests.testErrorDiagnosticMessage()); - this.MessagePrefixes.set(DiagnosticMessageType.Fail, localize.UnitTests.testFailDiagnosticMessage()); - this.MessagePrefixes.set(DiagnosticMessageType.Skipped, localize.UnitTests.testSkippedDiagnosticMessage()); - this.MessagePrefixes.set(DiagnosticMessageType.Pass, ''); - } - public getMessagePrefix(status: TestStatus): string { - return this.MessagePrefixes.get(this.MessageTypes.get(status)); - } - public getSeverity(unitTestSeverity: PythonUnitTestMessageSeverity): DiagnosticSeverity { - return this.MessageSeverities.get(unitTestSeverity); - } -} diff --git a/src/client/unittests/common/services/workspaceTestManagerService.ts b/src/client/unittests/common/services/workspaceTestManagerService.ts deleted file mode 100644 index 443c0614bf1f..000000000000 --- a/src/client/unittests/common/services/workspaceTestManagerService.ts +++ /dev/null @@ -1,55 +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<string, ITestManagerService>(); - 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/unittests/common/testUtils.ts b/src/client/unittests/common/testUtils.ts deleted file mode 100644 index feb4d7c9be50..000000000000 --- a/src/client/unittests/common/testUtils.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { inject, injectable, named } from 'inversify'; -import * as path from 'path'; -import { Uri, window, workspace } from 'vscode'; -import { IApplicationShell, ICommandManager } from '../../common/application/types'; -import * as constants from '../../common/constants'; -import { IUnitTestSettings, Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { CommandSource } from './constants'; -import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor'; -import { ITestsHelper, ITestVisitor, TestFile, TestFolder, TestProvider, Tests, TestSettingsPropertyNames, TestsToRun, UnitTestProduct } from './types'; - -export async function selectTestWorkspace(): Promise<Uri | undefined> { - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { - return undefined; - } else if (workspace.workspaceFolders.length === 1) { - return workspace.workspaceFolders[0].uri; - } else { - // tslint:disable-next-line:no-any prefer-type-cast - const workspaceFolder = await (window as any).showWorkspaceFolderPick({ placeHolder: 'Select a workspace' }); - return workspaceFolder ? workspaceFolder.uri : undefined; - } -} - -export function extractBetweenDelimiters(content: string, startDelimiter: string, endDelimiter: string): string { - content = content.substring(content.indexOf(startDelimiter) + startDelimiter.length); - return content.substring(0, content.lastIndexOf(endDelimiter)); -} - -export function convertFileToPackage(filePath: string): string { - const lastIndex = filePath.lastIndexOf('.'); - return filePath.substring(0, lastIndex).replace(/\//g, '.').replace(/\\/g, '.'); -} - -@injectable() -export class TestsHelper implements ITestsHelper { - private readonly appShell: IApplicationShell; - private readonly commandManager: ICommandManager; - constructor(@inject(ITestVisitor) @named('TestFlatteningVisitor') private flatteningVisitor: TestFlatteningVisitor, - @inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.appShell = serviceContainer.get<IApplicationShell>(IApplicationShell); - this.commandManager = serviceContainer.get<ICommandManager>(ICommandManager); - } - public parseProviderName(product: UnitTestProduct): TestProvider { - switch (product) { - case Product.nosetest: return 'nosetest'; - case Product.pytest: return 'pytest'; - case Product.unittest: return 'unittest'; - default: { - throw new Error(`Unknown Test Product ${product}`); - } - } - } - public parseProduct(provider: TestProvider): UnitTestProduct { - switch (provider) { - case 'nosetest': return Product.nosetest; - case 'pytest': return Product.pytest; - case 'unittest': return Product.unittest; - default: { - throw new Error(`Unknown Test Provider ${provider}`); - } - } - } - public getSettingsPropertyNames(product: UnitTestProduct): TestSettingsPropertyNames { - const id = this.parseProviderName(product); - switch (id) { - case 'pytest': { - return { - argsName: 'pyTestArgs' as keyof IUnitTestSettings, - pathName: 'pyTestPath' as keyof IUnitTestSettings, - enabledName: 'pyTestEnabled' as keyof IUnitTestSettings - }; - } - case 'nosetest': { - return { - argsName: 'nosetestArgs' as keyof IUnitTestSettings, - pathName: 'nosetestPath' as keyof IUnitTestSettings, - enabledName: 'nosetestsEnabled' as keyof IUnitTestSettings - }; - } - case 'unittest': { - return { - argsName: 'unittestArgs' as keyof IUnitTestSettings, - enabledName: 'unittestEnabled' as keyof IUnitTestSettings - }; - } - default: { - throw new Error(`Unknown Test Provider '${product}'`); - } - } - } - public flattenTestFiles(testFiles: TestFile[]): Tests { - testFiles.forEach(testFile => this.flatteningVisitor.visitTestFile(testFile)); - - // tslint:disable-next-line:no-object-literal-type-assertion - const tests = <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); - - return tests; - } - public placeTestFilesIntoFolders(tests: Tests): void { - // First get all the unique folders - const folders: string[] = []; - tests.testFiles.forEach(file => { - const dir = path.dirname(file.name); - if (folders.indexOf(dir) === -1) { - folders.push(dir); - } - }); - - tests.testFolders = []; - const folderMap = new Map<string, TestFolder>(); - folders.sort(); - - 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 = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 }; - folderMap.set(newPath, testFolder); - if (parentFolder) { - parentFolder!.folders.push(testFolder); - } else { - tests.rootTestFolders.push(testFolder); - } - tests.testFiles.filter(fl => path.dirname(fl.name) === 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. - // tslint:disable-next-line:no-object-literal-type-assertion - return <TestsToRun>{ testFile: [{ name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 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; - } -} diff --git a/src/client/unittests/common/testVisitors/flatteningVisitor.ts b/src/client/unittests/common/testVisitors/flatteningVisitor.ts deleted file mode 100644 index c59d6f9d5cce..000000000000 --- a/src/client/unittests/common/testVisitors/flatteningVisitor.ts +++ /dev/null @@ -1,67 +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<string, FlattenedTestFunction>(); - // tslint:disable-next-line:variable-name - private _flattenedTestSuites = new Map<string, FlattenedTestSuite>(); - 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/unittests/common/testVisitors/folderGenerationVisitor.ts b/src/client/unittests/common/testVisitors/folderGenerationVisitor.ts deleted file mode 100644 index df5a43e6adf3..000000000000 --- a/src/client/unittests/common/testVisitors/folderGenerationVisitor.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { injectable } from 'inversify'; -import * as path from 'path'; -import { ITestVisitor, TestFile, TestFolder, TestFunction, TestSuite } from '../types'; - -@injectable() -export class TestFolderGenerationVisitor implements ITestVisitor { - // tslint:disable-next-line:variable-name - private _testFolders: TestFolder[] = []; - // tslint:disable-next-line:variable-name - private _rootTestFolders: TestFolder[] = []; - private folderMap = new Map<string, TestFolder>(); - public get testFolders(): Readonly<TestFolder[]> { - return [...this._testFolders]; - } - public get rootTestFolders(): Readonly<TestFolder[]> { - return [...this._rootTestFolders]; - } - // 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 { - // First get all the unique folders - const dir = path.dirname(testFile.name); - if (this.folderMap.has(dir)) { - const folder = this.folderMap.get(dir)!; - folder.testFiles.push(testFile); - return; - } - - dir.split(path.sep).reduce((accumulatedPath, currentName, index) => { - let newPath = currentName; - let parentFolder: TestFolder | undefined; - if (accumulatedPath.length > 0) { - parentFolder = this.folderMap.get(accumulatedPath); - newPath = path.join(accumulatedPath, currentName); - } - if (!this.folderMap.has(newPath)) { - const testFolder: TestFolder = { name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0 }; - this.folderMap.set(newPath, testFolder); - if (parentFolder) { - parentFolder.folders.push(testFolder); - } else { - this._rootTestFolders.push(testFolder); - } - this._testFolders.push(testFolder); - } - return newPath; - }, ''); - - // tslint:disable-next-line:no-non-null-assertion - this.folderMap.get(dir)!.testFiles.push(testFile); - } - // tslint:disable-next-line:no-empty - public visitTestFolder(testFile: TestFolder) { } -} diff --git a/src/client/unittests/common/testVisitors/resultResetVisitor.ts b/src/client/unittests/common/testVisitors/resultResetVisitor.ts deleted file mode 100644 index 6929d9386fa9..000000000000 --- a/src/client/unittests/common/testVisitors/resultResetVisitor.ts +++ /dev/null @@ -1,39 +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.status = TestStatus.Unknown; - } -} diff --git a/src/client/unittests/common/types.ts b/src/client/unittests/common/types.ts deleted file mode 100644 index db5f373e9f5b..000000000000 --- a/src/client/unittests/common/types.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { CancellationToken, DiagnosticCollection, Disposable, OutputChannel, Uri } from 'vscode'; -import { IUnitTestSettings, Product } from '../../common/types'; -import { IPythonUnitTestMessage } from '../types'; -import { CommandSource } from './constants'; - -export type TestProvider = 'nosetest' | 'pytest' | 'unittest'; - -export type TestDiscoveryOptions = { - workspaceFolder: Uri; - cwd: string; - args: string[]; - token: CancellationToken; - ignoreCache: boolean; - outChannel: OutputChannel; -}; - -export type TestRunOptions = { - workspaceFolder: Uri; - cwd: string; - tests: Tests; - args: string[]; - testsToRun?: TestsToRun; - token: CancellationToken; - outChannel?: OutputChannel; - debug?: boolean; -}; - -export type UnitTestParserOptions = TestDiscoveryOptions & { startDirectory: string }; - -export type TestFolder = TestResult & { - name: string; - testFiles: TestFile[]; - nameToRun: string; - status?: TestStatus; - folders: TestFolder[]; -}; - -export type TestFile = TestResult & { - name: string; - fullPath: string; - functions: TestFunction[]; - suites: TestSuite[]; - nameToRun: string; - xmlName: string; - status?: TestStatus; - errorsWhenDiscovering?: string; -}; - -export type TestSuite = TestResult & { - name: string; - functions: TestFunction[]; - suites: TestSuite[]; - isUnitTest: Boolean; - isInstance: Boolean; - nameToRun: string; - xmlName: string; - status?: TestStatus; -}; - -export type TestFunction = TestResult & { - name: string; - nameToRun: string; - status?: TestStatus; -}; - -export type TestResult = Node & { - passed?: boolean; - time: number; - line?: number; - file?: string; - message?: string; - traceback?: string; - functionsPassed?: number; - functionsFailed?: number; - functionsDidNotRun?: number; -}; - -export type Node = { - expanded?: Boolean; -}; - -export type FlattenedTestFunction = { - testFunction: TestFunction; - parentTestSuite?: TestSuite; - parentTestFile: TestFile; - xmlClassName: string; -}; - -export type FlattenedTestSuite = { - testSuite: TestSuite; - parentTestSuite?: 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[]; -}; - -export enum TestStatus { - Unknown, - Discovering, - Idle, - Running, - Fail, - Error, - Skipped, - Pass -} - -export type TestsToRun = { - testFolder?: TestFolder[]; - testFile?: TestFile[]; - testSuite?: TestSuite[]; - testFunction?: TestFunction[]; -}; - -export type UnitTestProduct = Product.nosetest | Product.pytest | Product.unittest; - -export const ITestConfigSettingsService = Symbol('ITestConfigSettingsService'); -export interface ITestConfigSettingsService { - updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]): Promise<void>; - enable(testDirectory: string | Uri, product: UnitTestProduct): Promise<void>; - disable(testDirectory: string | Uri, product: UnitTestProduct): Promise<void>; -} - -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 type TestSettingsPropertyNames = { - enabledName: keyof IUnitTestSettings; - argsName: keyof IUnitTestSettings; - pathName?: keyof IUnitTestSettings; -}; - -export const ITestsHelper = Symbol('ITestsHelper'); - -export interface ITestsHelper { - parseProviderName(product: UnitTestProduct): TestProvider; - parseProduct(provider: TestProvider): UnitTestProduct; - getSettingsPropertyNames(product: Product): TestSettingsPropertyNames; - flattenTestFiles(testFiles: TestFile[]): Tests; - placeTestFilesIntoFolders(tests: Tests): void; - displayTestErrorMessage(message: string): void; - shouldRunAllTests(testsToRun?: TestsToRun): boolean; - mergeTests(items: Tests[]): Tests; -} - -export const ITestVisitor = Symbol('ITestVisitor'); - -export interface ITestVisitor { - visitTestFunction(testFunction: TestFunction): void; - visitTestSuite(testSuite: TestSuite): void; - visitTestFile(testFile: TestFile): void; - visitTestFolder(testFile: TestFolder): void; -} - -export const ITestCollectionStorageService = Symbol('ITestCollectionStorageService'); - -export interface ITestCollectionStorageService extends Disposable { - getTests(wkspace: Uri): Tests | undefined; - storeTests(wkspace: Uri, tests: Tests | null | undefined): void; -} - -export const ITestResultsService = Symbol('ITestResultsService'); - -export interface ITestResultsService { - resetResults(tests: Tests): void; - updateResults(tests: Tests): void; -} - -export type LaunchOptions = { - cwd: string; - args: string[]; - testProvider: TestProvider; - token?: CancellationToken; - outChannel?: OutputChannel; -}; - -export const ITestDebugLauncher = Symbol('ITestDebugLauncher'); - -export interface ITestDebugLauncher { - launchDebugger(options: LaunchOptions): Promise<void>; -} - -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; - stop(): void; - resetTestResults(): void; - discoverTests(cmdSource: CommandSource, ignoreCache?: boolean, quietMode?: boolean, userInitiated?: boolean): Promise<Tests>; - runTest(cmdSource: CommandSource, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<Tests>; -} - -export const ITestDiscoveryService = Symbol('ITestDiscoveryService'); - -export interface ITestDiscoveryService { - discoverTests(options: TestDiscoveryOptions): Promise<Tests>; -} - -export const ITestsParser = Symbol('ITestsParser'); -export interface ITestsParser { - parse(content: string, options: ParserOptions): Tests; -} - -export type ParserOptions = TestDiscoveryOptions; - -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<number>; - stop(): void; -} - -export type Options = { - workspaceFolder: Uri; - cwd: string; - args: string[]; - outChannel?: OutputChannel; - token: CancellationToken; -}; - -export const ITestRunner = Symbol('ITestRunner'); -export interface ITestRunner { - run(testProvider: TestProvider, options: Options): Promise<string>; -} - -export enum PassCalculationFormulae { - pytest, - nosetests -} - -export const IXUnitParser = Symbol('IXUnitParser'); -export interface IXUnitParser { - updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise<void>; -} - -export type PythonVersionInformation = { - major: number; - minor: number; -}; - -export const ITestMessageService = Symbol('ITestMessageService'); -export interface ITestMessageService { - getFilteredTestMessages(rootDirectory: string, testResults: Tests): Promise<IPythonUnitTestMessage[]>; -} diff --git a/src/client/unittests/common/xUnitParser.ts b/src/client/unittests/common/xUnitParser.ts deleted file mode 100644 index 1e71ce076d7b..000000000000 --- a/src/client/unittests/common/xUnitParser.ts +++ /dev/null @@ -1,144 +0,0 @@ -import * as fs from 'fs'; -import { injectable } from 'inversify'; -import { IXUnitParser, PassCalculationFormulae, Tests, TestStatus } 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 { - public updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise<void> { - return updateResultsFromXmlLogFile(tests, outputXmlFile, passCalculationFormulae); - } -} -export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise<void> { - // tslint:disable-next-line:no-any - return new Promise<any>((resolve, reject) => { - fs.readFile(outputXmlFile, 'utf8', (err, data) => { - if (err) { - return reject(err); - } - // tslint:disable-next-line:no-require-imports - const xml2js = require('xml2js'); - xml2js.parseString(data, (error, parserResult) => { - if (error) { - return reject(error); - } - - const testSuiteResult: TestSuiteResult = parserResult.testsuite; - tests.summary.errors = getSafeInt(testSuiteResult.$.errors); - tests.summary.failures = getSafeInt(testSuiteResult.$.failures); - tests.summary.skipped = getSafeInt(testSuiteResult.$.skips ? testSuiteResult.$.skips : testSuiteResult.$.skip); - const testCount = getSafeInt(testSuiteResult.$.tests); - - switch (passCalculationFormulae) { - case PassCalculationFormulae.pytest: { - tests.summary.passed = testCount - tests.summary.failures - tests.summary.skipped - tests.summary.errors; - break; - } - case PassCalculationFormulae.nosetests: { - tests.summary.passed = testCount - tests.summary.failures - tests.summary.skipped - tests.summary.errors; - break; - } - default: { - throw new Error('Unknown Test Pass Calculation'); - } - } - - if (!Array.isArray(testSuiteResult.testcase)) { - return resolve(); - } - - testSuiteResult.testcase.forEach((testcase: TestCaseResult) => { - const xmlClassName = testcase.$.classname.replace(/\(\)/g, '').replace(/\.\./g, '.').replace(/\.\./g, '.').replace(/\.+$/, ''); - const result = tests.testFunctions.find(fn => fn.xmlClassName === xmlClassName && fn.testFunction.name === testcase.$.name); - if (!result) { - // 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) { - fileTest.status = TestStatus.Error; - fileTest.passed = false; - fileTest.message = testcase.error[0].$.message; - fileTest.traceback = testcase.error[0]._; - } - return; - } - - result.testFunction.line = getSafeInt(testcase.$.line, null); - result.testFunction.file = testcase.$.file; - result.testFunction.time = parseFloat(testcase.$.time); - result.testFunction.passed = true; - result.testFunction.status = TestStatus.Pass; - - if (testcase.failure) { - result.testFunction.status = TestStatus.Fail; - result.testFunction.passed = false; - result.testFunction.message = testcase.failure[0].$.message; - result.testFunction.traceback = testcase.failure[0]._; - } - - if (testcase.error) { - result.testFunction.status = TestStatus.Error; - result.testFunction.passed = false; - result.testFunction.message = testcase.error[0].$.message; - result.testFunction.traceback = testcase.error[0]._; - } - - if (testcase.skipped) { - result.testFunction.status = TestStatus.Skipped; - result.testFunction.passed = undefined; - result.testFunction.message = testcase.skipped[0].$.message; - result.testFunction.traceback = ''; - } - }); - - resolve(); - }); - }); - }); -} diff --git a/src/client/unittests/configuration.ts b/src/client/unittests/configuration.ts deleted file mode 100644 index 3772a90241a3..000000000000 --- a/src/client/unittests/configuration.ts +++ /dev/null @@ -1,110 +0,0 @@ -'use strict'; - -import { inject, injectable } from 'inversify'; -import { OutputChannel, Uri } from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../common/application/types'; -import { IConfigurationService, IInstaller, IOutputChannel, Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { TEST_OUTPUT_CHANNEL } from './common/constants'; -import { UnitTestProduct } from './common/types'; -import { ITestConfigurationManagerFactory, IUnitTestConfigurationService } from './types'; - -@injectable() -export class UnitTestConfigurationService implements IUnitTestConfigurationService { - private readonly configurationService: IConfigurationService; - private readonly appShell: IApplicationShell; - private readonly installer: IInstaller; - private readonly outputChannel: OutputChannel; - private readonly workspaceService: IWorkspaceService; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.configurationService = serviceContainer.get<IConfigurationService>(IConfigurationService); - this.appShell = serviceContainer.get<IApplicationShell>(IApplicationShell); - this.installer = serviceContainer.get<IInstaller>(IInstaller); - this.outputChannel = serviceContainer.get<OutputChannel>(IOutputChannel, TEST_OUTPUT_CHANNEL); - this.workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService); - } - public async displayTestFrameworkError(wkspace: Uri): Promise<void> { - const settings = this.configurationService.getSettings(wkspace); - let enabledCount = settings.unitTest.pyTestEnabled ? 1 : 0; - enabledCount += settings.unitTest.nosetestsEnabled ? 1 : 0; - enabledCount += settings.unitTest.unittestEnabled ? 1 : 0; - if (enabledCount > 1) { - return this.promptToEnableAndConfigureTestFramework(wkspace, this.installer, this.outputChannel, '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, this.installer, this.outputChannel); - } - return Promise.reject(null); - } - } - public async selectTestRunner(placeHolderMessage: string): Promise<UnitTestProduct | undefined> { - 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: 'Can run unittest (including trial) and nose test suites out of the box', - // 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 = { - 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 enableTest(wkspace: Uri, product: UnitTestProduct) { - const factory = this.serviceContainer.get<ITestConfigurationManagerFactory>(ITestConfigurationManagerFactory); - const configMgr = factory.create(wkspace, product); - const pythonConfig = this.workspaceService.getConfiguration('python', wkspace); - if (pythonConfig.get<boolean>('unitTest.promptToConfigure')) { - return configMgr.enable(); - } - return pythonConfig.update('unitTest.promptToConfigure', undefined).then(() => { - return configMgr.enable(); - }, reason => { - return configMgr.enable().then(() => Promise.reject(reason)); - }); - } - - private async promptToEnableAndConfigureTestFramework(wkspace: Uri, installer: IInstaller, outputChannel: OutputChannel, messageToDisplay: string = 'Select a test framework/tool to enable', enableOnly: boolean = false) { - const selectedTestRunner = await this.selectTestRunner(messageToDisplay); - if (typeof selectedTestRunner !== 'number') { - return Promise.reject(null); - } - const factory = this.serviceContainer.get<ITestConfigurationManagerFactory>(ITestConfigurationManagerFactory); - const configMgr = factory.create(wkspace, selectedTestRunner); - if (enableOnly) { - // Ensure others are disabled - [Product.unittest, Product.pytest, Product.nosetest] - .filter(prod => selectedTestRunner !== prod) - .forEach(prod => { - factory.create(wkspace, prod).disable() - .catch(ex => console.error('Python Extension: createTestConfigurationManager.disable', ex)); - }); - return configMgr.enable(); - } - - // 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. - return configMgr.configure(wkspace) - .then(() => this.enableTest(wkspace, selectedTestRunner)) - .catch(reason => { return this.enableTest(wkspace, selectedTestRunner).then(() => Promise.reject(reason)); }); - } -} diff --git a/src/client/unittests/configurationFactory.ts b/src/client/unittests/configurationFactory.ts deleted file mode 100644 index ee29d6d8c1d6..000000000000 --- a/src/client/unittests/configurationFactory.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 { 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 { ITestConfigurationManagerFactory } from './types'; -import * as unittest from './unittest/testConfigurationManager'; - -@injectable() -export class TestConfigurationManagerFactory implements ITestConfigurationManagerFactory { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } - public create(wkspace: Uri, product: Product) { - switch (product) { - case Product.unittest: { - return new unittest.ConfigurationManager(wkspace, this.serviceContainer); - } - case Product.pytest: { - return new pytest.ConfigurationManager(wkspace, this.serviceContainer); - } - case Product.nosetest: { - return new nose.ConfigurationManager(wkspace, this.serviceContainer); - } - default: { - throw new Error('Invalid test configuration'); - } - } - } - -} diff --git a/src/client/unittests/display/main.ts b/src/client/unittests/display/main.ts deleted file mode 100644 index b9dfe431bddb..000000000000 --- a/src/client/unittests/display/main.ts +++ /dev/null @@ -1,197 +0,0 @@ -'use strict'; -import { inject, injectable } from 'inversify'; -import { Event, EventEmitter, StatusBarAlignment, StatusBarItem } from 'vscode'; -import { IApplicationShell } from '../../common/application/types'; -import * as constants from '../../common/constants'; -import { isNotInstalledError } from '../../common/helpers'; -import { IConfigurationService } from '../../common/types'; -import { noop } from '../../common/utils/misc'; -import { IServiceContainer } from '../../ioc/types'; -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; - private _enabled: boolean = false; - private progressPrefix!: string; - private readonly didChange = new EventEmitter<void>(); - private readonly appShell: IApplicationShell; - private readonly testsHelper: ITestsHelper; - public get onDidChange(): Event<void> { - return this.didChange.event; - } - - // tslint:disable-next-line:no-any - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.appShell = serviceContainer.get<IApplicationShell>(IApplicationShell); - this.statusBar = this.appShell.createStatusBarItem(StatusBarAlignment.Left); - this.testsHelper = serviceContainer.get<ITestsHelper>(ITestsHelper); - } - 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<Tests>, 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<Tests>, 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[] = []; - let foreColor = ''; - - if (tests.summary.passed > 0) { - statusText.push(`${constants.Octicons.Test_Pass} ${tests.summary.passed}`); - toolTip.push(`${tests.summary.passed} Passed`); - foreColor = '#66ff66'; - } - if (tests.summary.skipped > 0) { - statusText.push(`${constants.Octicons.Test_Skip} ${tests.summary.skipped}`); - toolTip.push(`${tests.summary.skipped} Skipped`); - foreColor = '#66ff66'; - } - if (tests.summary.failures > 0) { - statusText.push(`${constants.Octicons.Test_Fail} ${tests.summary.failures}`); - toolTip.push(`${tests.summary.failures} Failed`); - foreColor = 'yellow'; - } - 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' : ''}`); - foreColor = 'yellow'; - } - 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.color = foreColor; - 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<any> { - 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(), 150); - } - private updateProgressTicker() { - const text = `${this.progressPrefix} ${this.ticker[this.discoverCounter % 7]}`; - this.discoverCounter += 1; - this.statusBar.text = text; - } - private clearProgressTicker() { - if (this.progressTimeout) { - clearInterval(this.progressTimeout); - } - this.progressTimeout = null; - this.discoverCounter = 0; - } - - // tslint:disable-next-line:no-any - private async disableTests(): Promise<any> { - const configurationService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); - const settingsToDisable = ['unitTest.promptToConfigure', 'unitTest.pyTestEnabled', - 'unitTest.unittestEnabled', 'unitTest.nosetestsEnabled']; - - for (const setting of settingsToDisable) { - await configurationService.updateSetting(setting, false).catch(noop); - } - } - - 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.', 'Disable Tests').then(item => { - if (item === 'Disable Tests') { - this.disableTests() - .catch(ex => console.error('Python Extension: disableTests', ex)); - } - }); - } - } - - // 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(); - this.statusBar.color = 'yellow'; - 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/unittests/display/picker.ts b/src/client/unittests/display/picker.ts deleted file mode 100644 index 452dbf83df90..000000000000 --- a/src/client/unittests/display/picker.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { commands, QuickPickItem, Uri } from 'vscode'; -import { IApplicationShell } from '../../common/application/types'; -import * as constants from '../../common/constants'; -import { noop } from '../../common/utils/misc'; -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) serviceRegistry: IServiceContainer) { - this.testCollectionStorage = serviceRegistry.get<ITestCollectionStorageService>(ITestCollectionStorageService); - this.appShell = serviceRegistry.get<IApplicationShell>(IApplicationShell); - } - public displayStopTestUI(workspace: Uri, message: string) { - this.appShell.showQuickPick([message]).then(item => { - if (item === message) { - commands.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(cmdSource, wkspace, item, false) : noop()); - } - public selectTestFunction(rootDirectory: string, tests: Tests): Promise<FlattenedTestFunction> { - return new Promise<FlattenedTestFunction>((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<TestFile> { - return new Promise<TestFile>((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 testFile = tests.testFiles.find(item => item.name === fileName || 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); - }); - - this.appShell.showQuickPick(buildItemsForFunctions(rootDirectory, flattenedFunctions, undefined, undefined, debug), - { matchOnDescription: true, matchOnDetail: true }) - .then(testItem => testItem ? onItemSelected(cmdSource, wkspace, testItem, debug) : noop()); - } -} - -enum Type { - RunAll = 0, - ReDiscover = 1, - RunFailed = 2, - RunFolder = 3, - RunFile = 4, - RunClass = 5, - RunMethod = 6, - ViewTestOutput = 7, - Null = 8, - SelectAndRunMethod = 9, - DebugMethod = 10 -} -const statusIconMapping = new Map<TestStatus, string>(); -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; -}; - -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 Unit Tests', type: Type.RunAll }); - items.push({ description: '', label: 'Discover Unit Tests', type: Type.ReDiscover }); - items.push({ description: '', label: 'Run Unit Test Method ...', type: Type.SelectAndRunMethod }); - - const summary = getSummary(tests); - items.push({ description: '', label: 'View Unit 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 = {}; -statusSortPrefix[TestStatus.Error] = '1'; -statusSortPrefix[TestStatus.Fail] = '2'; -statusSortPrefix[TestStatus.Skipped] = '3'; -statusSortPrefix[TestStatus.Pass] = '4'; - -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; -} -function onItemSelected(cmdSource: CommandSource, wkspace: Uri, selection: TestItem, debug?: boolean) { - if (!selection || typeof selection.type !== 'number') { - return; - } - let cmd = ''; - // tslint:disable-next-line:no-any - const args: any[] = [undefined, cmdSource, wkspace]; - switch (selection.type) { - case Type.Null: { - return; - } - case Type.RunAll: { - cmd = constants.Commands.Tests_Run; - break; - } - case Type.ReDiscover: { - cmd = constants.Commands.Tests_Discover; - break; - } - case Type.ViewTestOutput: { - cmd = constants.Commands.Tests_ViewOutput; - break; - } - case Type.RunFailed: { - cmd = constants.Commands.Tests_Run_Failed; - break; - } - case Type.SelectAndRunMethod: { - cmd = debug ? constants.Commands.Tests_Select_And_Debug_Method : constants.Commands.Tests_Select_And_Run_Method; - break; - } - case Type.RunMethod: { - cmd = constants.Commands.Tests_Run; - // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion - args.push({ testFunction: [selection.fn!.testFunction] } as TestsToRun); - break; - } - case Type.DebugMethod: { - cmd = constants.Commands.Tests_Debug; - // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion - args.push({ testFunction: [selection.fn!.testFunction] } as TestsToRun); - args.push(true); - break; - } - default: { - return; - } - } - - commands.executeCommand(cmd, ...args); -} diff --git a/src/client/unittests/main.ts b/src/client/unittests/main.ts deleted file mode 100644 index c349f21738d6..000000000000 --- a/src/client/unittests/main.ts +++ /dev/null @@ -1,329 +0,0 @@ -'use strict'; - -// tslint:disable:no-duplicate-imports no-unnecessary-callback-wrapper - -import { inject, injectable } from 'inversify'; -import { ConfigurationChangeEvent, Disposable, OutputChannel, TextDocument, Uri } from 'vscode'; -import * as vscode from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; -import * as constants from '../common/constants'; -import { IConfigurationService, IDisposableRegistry, ILogger, IOutputChannel } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { UNITTEST_STOP, UNITTEST_VIEW_OUTPUT } from '../telemetry/constants'; -import { sendTelemetryEvent } from '../telemetry/index'; -import { activateCodeLenses } from './codeLenses/main'; -import { CANCELLATION_REASON, CommandSource, TEST_OUTPUT_CHANNEL } from './common/constants'; -import { selectTestWorkspace } from './common/testUtils'; -import { ITestCollectionStorageService, ITestManager, IWorkspaceTestManagerService, TestFile, TestFunction, TestStatus, TestsToRun } from './common/types'; -import { ITestDisplay, ITestResultDisplay, IUnitTestConfigurationService, IUnitTestManagementService } from './types'; - -@injectable() -export class UnitTestManagementService implements IUnitTestManagementService, Disposable { - private readonly outputChannel: vscode.OutputChannel; - private readonly disposableRegistry: Disposable[]; - private workspaceTestManagerService?: IWorkspaceTestManagerService; - private documentManager: IDocumentManager; - private workspaceService: IWorkspaceService; - private testResultDisplay?: ITestResultDisplay; - private autoDiscoverTimer?: NodeJS.Timer; - private configChangedTimer?: NodeJS.Timer; - private readonly onDidChange: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.disposableRegistry = serviceContainer.get<Disposable[]>(IDisposableRegistry); - this.outputChannel = serviceContainer.get<OutputChannel>(IOutputChannel, TEST_OUTPUT_CHANNEL); - this.workspaceService = serviceContainer.get<IWorkspaceService>(IWorkspaceService); - this.documentManager = serviceContainer.get<IDocumentManager>(IDocumentManager); - - this.disposableRegistry.push(this); - } - public dispose() { - if (this.workspaceTestManagerService) { - this.workspaceTestManagerService.dispose(); - } - } - public async activate(): Promise<void> { - this.workspaceTestManagerService = this.serviceContainer.get<IWorkspaceTestManagerService>(IWorkspaceTestManagerService); - - this.registerHandlers(); - this.registerCommands(); - this.autoDiscoverTests() - .catch(ex => this.serviceContainer.get<ILogger>(ILogger).logError('Failed to auto discover tests upon activation', ex)); - } - public async activateCodeLenses(symboldProvider: vscode.DocumentSymbolProvider): Promise<void> { - const testCollectionStorage = this.serviceContainer.get<ITestCollectionStorageService>(ITestCollectionStorageService); - this.disposableRegistry.push(activateCodeLenses(this.onDidChange, symboldProvider, testCollectionStorage)); - } - public async getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise<ITestManager | undefined | void> { - let wkspace: Uri | undefined; - if (resource) { - const wkspaceFolder = this.workspaceService.getWorkspaceFolder(resource); - wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; - } else { - wkspace = await selectTestWorkspace(); - } - if (!wkspace) { - return; - } - const testManager = this.workspaceTestManagerService!.getTestManager(wkspace); - if (testManager) { - return testManager; - } - if (displayTestNotConfiguredMessage) { - const configurationService = this.serviceContainer.get<IUnitTestConfigurationService>(IUnitTestConfigurationService); - await configurationService.displayTestFrameworkError(wkspace); - } - } - public async configurationChangeHandler(e: 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; - } - - const workspaceUri = this.workspaceService.workspaceFolders![0].uri; - if (!e.affectsConfiguration('python.unitTest', workspaceUri)) { - return; - } - const settings = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(workspaceUri); - if (!settings.unitTest.nosetestsEnabled && !settings.unitTest.pyTestEnabled && !settings.unitTest.unittestEnabled) { - if (this.testResultDisplay) { - this.testResultDisplay.enabled = false; - } - // tslint:disable-next-line:no-suspicious-comment - // TODO: Why are we disposing, what happens when tests are enabled. - if (this.workspaceTestManagerService) { - this.workspaceTestManagerService.dispose(); - } - return; - } - if (this.testResultDisplay) { - this.testResultDisplay.enabled = true; - } - this.autoDiscoverTests() - .catch(ex => this.serviceContainer.get<ILogger>(ILogger).logError('Failed to auto discover tests upon activation', ex)); - } - - public async discoverTestsForDocument(doc: TextDocument): Promise<void> { - 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); - } - this.autoDiscoverTimer = setTimeout(() => this.discoverTests(CommandSource.auto, doc.uri, true, false, true), 1000); - } - public async autoDiscoverTests() { - if (!this.workspaceService.hasWorkspaceFolders) { - return; - } - const configurationService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); - const settings = configurationService.getSettings(); - if (!settings.unitTest.nosetestsEnabled && !settings.unitTest.pyTestEnabled && !settings.unitTest.unittestEnabled) { - return; - } - - // No need to display errors. - // tslint:disable-next-line:no-empty - this.discoverTests(CommandSource.auto, this.workspaceService.workspaceFolders![0].uri, true).catch(() => { }); - } - public async discoverTests(cmdSource: CommandSource, resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean, quietMode?: boolean) { - const testManager = await this.getTestManager(true, resource); - if (!testManager) { - return; - } - - if (testManager.status === TestStatus.Discovering || testManager.status === TestStatus.Running) { - return; - } - - if (!this.testResultDisplay) { - this.testResultDisplay = this.serviceContainer.get<ITestResultDisplay>(ITestResultDisplay); - this.testResultDisplay.onDidChange(() => this.onDidChange.fire()); - } - const discoveryPromise = testManager.discoverTests(cmdSource, ignoreCache, quietMode, userInitiated); - this.testResultDisplay.displayDiscoverStatus(discoveryPromise, quietMode) - .catch(ex => console.error('Python Extension: displayDiscoverStatus', ex)); - await discoveryPromise; - } - public async stopTests(resource: Uri) { - sendTelemetryEvent(UNITTEST_STOP); - const testManager = await this.getTestManager(true, resource); - if (testManager) { - testManager.stop(); - } - } - public async displayStopUI(message: string): Promise<void> { - const testManager = await this.getTestManager(true); - if (!testManager) { - return; - } - - const testDisplay = this.serviceContainer.get<ITestDisplay>(ITestDisplay); - testDisplay.displayStopTestUI(testManager.workspaceFolder, message); - } - public async displayUI(cmdSource: CommandSource) { - const testManager = await this.getTestManager(true); - if (!testManager) { - return; - } - - const testDisplay = this.serviceContainer.get<ITestDisplay>(ITestDisplay); - testDisplay.displayTestUI(cmdSource, testManager.workspaceFolder); - } - public async displayPickerUI(cmdSource: CommandSource, file: Uri, testFunctions: TestFunction[], debug?: boolean) { - const testManager = await this.getTestManager(true, file); - if (!testManager) { - return; - } - - const testDisplay = this.serviceContainer.get<ITestDisplay>(ITestDisplay); - testDisplay.displayFunctionTestPickerUI(cmdSource, testManager.workspaceFolder, testManager.workingDirectory, file, testFunctions, debug); - } - public viewOutput(cmdSource: CommandSource) { - sendTelemetryEvent(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>(ITestCollectionStorageService); - const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; - const testDisplay = this.serviceContainer.get<ITestDisplay>(ITestDisplay); - const selectedTestFn = await testDisplay.selectTestFunction(testManager.workspaceFolder.fsPath, tests); - if (!selectedTestFn) { - return; - } - // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion - await this.runTestsImpl(cmdSource, testManager.workspaceFolder, { testFunction: [selectedTestFn.testFunction] } as TestsToRun, false, debug); - } - public async selectAndRunTestFile(cmdSource: CommandSource) { - const testManager = await this.getTestManager(true); - if (!testManager) { - return; - } - try { - await testManager.discoverTests(cmdSource, true, true, true); - } catch (ex) { - return; - } - - const testCollectionStorage = this.serviceContainer.get<ITestCollectionStorageService>(ITestCollectionStorageService); - const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; - const testDisplay = this.serviceContainer.get<ITestDisplay>(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>(ITestCollectionStorageService); - const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; - const testFiles = tests.testFiles.filter(testFile => { - return testFile.fullPath === this.documentManager.activeTextEditor!.document.uri.fsPath; - }); - if (testFiles.length < 1) { - return; - } - await this.runTestsImpl(cmdSource, testManager.workspaceFolder, { testFile: [testFiles[0]] }); - } - - public async runTestsImpl(cmdSource: CommandSource, resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug: boolean = false) { - const testManager = await this.getTestManager(true, resource); - if (!testManager) { - return; - } - - if (!this.testResultDisplay) { - this.testResultDisplay = this.serviceContainer.get<ITestResultDisplay>(ITestResultDisplay); - this.testResultDisplay.onDidChange(() => this.onDidChange.fire()); - } - - 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; - } - private registerCommands(): void { - const disposablesRegistry = this.serviceContainer.get<Disposable[]>(IDisposableRegistry); - const commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager); - - const disposables = [ - commandManager.registerCommand(constants.Commands.Tests_Discover, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => { - // Ignore the exceptions returned. - // This command will be invoked from other places of the extension. - this.discoverTests(cmdSource, resource, true, true).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, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun?: TestsToRun) => this.runTestsImpl(cmdSource, file, testToRun)), - commandManager.registerCommand(constants.Commands.Tests_Debug, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun: TestsToRun) => this.runTestsImpl(cmdSource, file, 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_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)) - ]; - - disposablesRegistry.push(...disposables); - } - private onDocumentSaved(doc: TextDocument) { - const settings = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(doc.uri); - if (!settings.unitTest.autoTestDiscoverOnSaveEnabled) { - return; - } - this.discoverTestsForDocument(doc).ignoreErrors(); - } - private registerHandlers() { - const documentManager = this.serviceContainer.get<IDocumentManager>(IDocumentManager); - - this.disposableRegistry.push(documentManager.onDidSaveTextDocument(this.onDocumentSaved.bind(this))); - this.disposableRegistry.push(this.workspaceService.onDidChangeConfiguration(e => { - if (this.configChangedTimer) { - clearTimeout(this.configChangedTimer); - } - this.configChangedTimer = setTimeout(() => this.configurationChangeHandler(e), 1000); - })); - } -} diff --git a/src/client/unittests/nosetest/main.ts b/src/client/unittests/nosetest/main.ts deleted file mode 100644 index 19aa8b125e9a..000000000000 --- a/src/client/unittests/nosetest/main.ts +++ /dev/null @@ -1,60 +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.unitTest.nosetestsEnabled; - } - constructor(workspaceFolder: Uri, rootDirectory: string, - @inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(NOSETEST_PROVIDER, Product.nosetest, workspaceFolder, rootDirectory, serviceContainer); - this.argsService = this.serviceContainer.get<IArgumentsService>(IArgumentsService, this.testProvider); - this.helper = this.serviceContainer.get<ITestsHelper>(ITestsHelper); - this.runner = this.serviceContainer.get<ITestManagerRunner>(ITestManagerRunner, this.testProvider); - } - public getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions { - const args = this.settings.unitTest.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<Tests> { - let args: string[]; - - const runAllTests = this.helper.shouldRunAllTests(testsToRun); - if (debug) { - args = this.argsService.filterArguments(this.settings.unitTest.nosetestArgs, runAllTests ? TestFilter.debugAll : TestFilter.debugSpecific); - } else { - args = this.argsService.filterArguments(this.settings.unitTest.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/unittests/nosetest/runner.ts b/src/client/unittests/nosetest/runner.ts deleted file mode 100644 index 692e038aca8b..000000000000 --- a/src/client/unittests/nosetest/runner.ts +++ /dev/null @@ -1,99 +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, PassCalculationFormulae, 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>(IArgumentsService, NOSETEST_PROVIDER); - this.argsHelper = serviceContainer.get<IArgumentsHelper>(IArgumentsHelper); - this.testRunner = serviceContainer.get<ITestRunner>(ITestRunner); - this.xUnitParser = this.serviceContainer.get<IXUnitParser>(IXUnitParser); - this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem); - } - public async runTest(testResultsService: ITestResultsService, options: TestRunOptions, _: ITestManager): Promise<Tests> { - 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>(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<Tests>(ex); - } finally { - deleteJUnitXmlFile(); - } - } - - private async updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise<Tests> { - await this.xUnitParser.updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.nosetests); - testResultsService.updateResults(tests); - return tests; - } - private async getUnitXmlFile(args: string[]): Promise<TemporaryFile> { - 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/unittests/nosetest/services/argsService.ts b/src/client/unittests/nosetest/services/argsService.ts deleted file mode 100644 index 8fbe92f41006..000000000000 --- a/src/client/unittests/nosetest/services/argsService.ts +++ /dev/null @@ -1,111 +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>(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/unittests/nosetest/services/discoveryService.ts b/src/client/unittests/nosetest/services/discoveryService.ts deleted file mode 100644 index 157b24d11257..000000000000 --- a/src/client/unittests/nosetest/services/discoveryService.ts +++ /dev/null @@ -1,41 +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>(IArgumentsService, NOSETEST_PROVIDER); - this.runner = this.serviceContainer.get<ITestRunner>(ITestRunner); - } - public async discoverTests(options: TestDiscoveryOptions): Promise<Tests> { - // 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<Tests>('cancelled'); - } - - return this.testParser.parse(data, options); - } -} diff --git a/src/client/unittests/nosetest/services/parserService.ts b/src/client/unittests/nosetest/services/parserService.ts deleted file mode 100644 index 634369db83c7..000000000000 --- a/src/client/unittests/nosetest/services/parserService.ts +++ /dev/null @@ -1,141 +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 { convertFileToPackage, extractBetweenDelimiters } from '../../common/testUtils'; -import { ITestsHelper, ITestsParser, ParserOptions, TestDiscoveryOptions, TestFile, TestFunction, Tests, TestStatus, 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); - } - - 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; - 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 = { - 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 <class \'')) { - const name = extractBetweenDelimiters(line, 'nose.selector: DEBUG: wantClass <class \'', '\'>? True'); - const clsName = path.extname(name).substring(1); - const testSuite: TestSuite = { - 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 = { - 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 <unbound method ')) { - const name = extractBetweenDelimiters(line, 'nose.selector: DEBUG: wantMethod <unbound method ', '>? True'); - const fnName = path.extname(name).substring(1); - const clsName = path.basename(name, path.extname(name)); - const fn: TestFunction = { - 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 <function ')) { - const name = extractBetweenDelimiters(line, 'nose.selector: DEBUG: wantFunction <function ', ' at '); - const fn: TestFunction = { - name: name, nameToRun: `${fileName}:${name}`, - time: 0, functionsFailed: 0, functionsPassed: 0 - }; - testFile.functions.push(fn); - return; - } - }); - } -} diff --git a/src/client/unittests/nosetest/testConfigurationManager.ts b/src/client/unittests/nosetest/testConfigurationManager.ts deleted file mode 100644 index 3003e42aa1db..000000000000 --- a/src/client/unittests/nosetest/testConfigurationManager.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as path from 'path'; -import { 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'; - -export class ConfigurationManager extends TestConfigurationManager { - constructor(workspace: Uri, serviceContainer: IServiceContainer) { - super(workspace, Product.nosetest, serviceContainer); - } - public async requiresUserToConfigure(wkspace: Uri): Promise<boolean> { - const fs = this.serviceContainer.get<IFileSystem>(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<void> { - 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/unittests/pytest/main.ts b/src/client/unittests/pytest/main.ts deleted file mode 100644 index bfae70e14419..000000000000 --- a/src/client/unittests/pytest/main.ts +++ /dev/null @@ -1,61 +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, IPythonUnitTestMessage, 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.unitTest.pyTestEnabled; - } - constructor(workspaceFolder: Uri, rootDirectory: string, - serviceContainer: IServiceContainer) { - super(PYTEST_PROVIDER, Product.pytest, workspaceFolder, rootDirectory, serviceContainer); - this.argsService = this.serviceContainer.get<IArgumentsService>(IArgumentsService, this.testProvider); - this.helper = this.serviceContainer.get<ITestsHelper>(ITestsHelper); - this.runner = this.serviceContainer.get<ITestManagerRunner>(ITestManagerRunner, this.testProvider); - this.testMessageService = this.serviceContainer.get<ITestMessageService>(ITestMessageService, this.testProvider); - } - public getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions { - const args = this.settings.unitTest.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<Tests> { - let args: string[]; - - const runAllTests = this.helper.shouldRunAllTests(testsToRun); - if (debug) { - args = this.argsService.filterArguments(this.settings.unitTest.pyTestArgs, runAllTests ? TestFilter.debugAll : TestFilter.debugSpecific); - } else { - args = this.argsService.filterArguments(this.settings.unitTest.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: IPythonUnitTestMessage[] = await this.testMessageService.getFilteredTestMessages(this.rootDirectory, testResults); - await this.updateDiagnostics(tests, messages); - return testResults; - } -} diff --git a/src/client/unittests/pytest/runner.ts b/src/client/unittests/pytest/runner.ts deleted file mode 100644 index f10784011273..000000000000 --- a/src/client/unittests/pytest/runner.ts +++ /dev/null @@ -1,92 +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, PassCalculationFormulae, TestRunOptions, Tests } from '../common/types'; -import { IArgumentsHelper, IArgumentsService, ITestManagerRunner } from '../types'; - -const JunitXmlArg = '--junitxml'; -@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>(IArgumentsService, PYTEST_PROVIDER); - this.argsHelper = serviceContainer.get<IArgumentsHelper>(IArgumentsHelper); - this.testRunner = serviceContainer.get<ITestRunner>(ITestRunner); - this.xUnitParser = this.serviceContainer.get<IXUnitParser>(IXUnitParser); - this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem); - } - public async runTest(testResultsService: ITestResultsService, options: TestRunOptions, _: ITestManager): Promise<Tests> { - 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 '--junixml' if it exists, and add it with our path. - const testArgs = this.argsService.filterArguments(args, [JunitXmlArg]); - testArgs.splice(0, 0, `${JunitXmlArg}=${xmlLogFile}`); - - // Positional arguments control the tests to be run. - testArgs.push(...testPaths); - - if (options.debug) { - const debugLauncher = this.serviceContainer.get<ITestDebugLauncher>(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<Tests>(ex); - } finally { - deleteJUnitXmlFile(); - } - } - - private async updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise<Tests> { - await this.xUnitParser.updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.pytest); - testResultsService.updateResults(tests); - return tests; - } - - private async getJUnitXmlFile(args: string[]): Promise<TemporaryFile> { - 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/unittests/pytest/services/argsService.ts b/src/client/unittests/pytest/services/argsService.ts deleted file mode 100644 index 17775ec50d1c..000000000000 --- a/src/client/unittests/pytest/services/argsService.ts +++ /dev/null @@ -1,160 +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', - '--assert', '--basetemp', '--capture', '--color', '--confcutdir', - '--cov', '--cov-config', '--cov-fail-under', '--cov-report', - '--deselect', '--dist', '--doctest-glob', - '--doctest-report', '--durations', '--ignore', '--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', '--cache-show', '--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', - '--strict', '--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>(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 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/unittests/pytest/services/discoveryService.ts b/src/client/unittests/pytest/services/discoveryService.ts deleted file mode 100644 index 57d9a3902e5d..000000000000 --- a/src/client/unittests/pytest/services/discoveryService.ts +++ /dev/null @@ -1,74 +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 { PYTEST_PROVIDER } from '../../common/constants'; -import { ITestDiscoveryService, ITestRunner, ITestsHelper, ITestsParser, Options, TestDiscoveryOptions, Tests } from '../../common/types'; -import { IArgumentsService, TestFilter } from '../../types'; - -@injectable() -export class TestDiscoveryService implements ITestDiscoveryService { - private argsService: IArgumentsService; - private helper: ITestsHelper; - private runner: ITestRunner; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(ITestsParser) @named(PYTEST_PROVIDER) private testParser: ITestsParser) { - this.argsService = this.serviceContainer.get<IArgumentsService>(IArgumentsService, PYTEST_PROVIDER); - this.helper = this.serviceContainer.get<ITestsHelper>(ITestsHelper); - this.runner = this.serviceContainer.get<ITestRunner>(ITestRunner); - } - public async discoverTests(options: TestDiscoveryOptions): Promise<Tests> { - 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); - } - private 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, '--collect-only'); - return args; - } - private async discoverTestsInTestDirectory(options: TestDiscoveryOptions): Promise<Tests> { - const token = options.token ? options.token : new CancellationTokenSource().token; - const runOptions: Options = { - args: options.args, - cwd: options.cwd, - workspaceFolder: options.workspaceFolder, - token, - outChannel: options.outChannel - }; - - const data = await this.runner.run(PYTEST_PROVIDER, runOptions); - if (options.token && options.token.isCancellationRequested) { - return Promise.reject<Tests>('cancelled'); - } - - return this.testParser.parse(data, options); - } -} diff --git a/src/client/unittests/pytest/services/parserService.ts b/src/client/unittests/pytest/services/parserService.ts deleted file mode 100644 index 1b795ff6fd50..000000000000 --- a/src/client/unittests/pytest/services/parserService.ts +++ /dev/null @@ -1,245 +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 { convertFileToPackage, extractBetweenDelimiters } from '../../common/testUtils'; -import { ITestsHelper, ITestsParser, ParserOptions, TestFile, TestFunction, Tests, TestSuite } from '../../common/types'; - -const DELIMITER = '\''; - -@injectable() -export class TestsParser implements ITestsParser { - - constructor(@inject(ITestsHelper) private testsHelper: ITestsHelper) { } - - public parse(content: string, options: ParserOptions): Tests { - const testFiles = this.getTestFiles(content, options); - return this.testsHelper.flattenTestFiles(testFiles); - } - - private getTestFiles(content: string, options: ParserOptions) { - let logOutputLines: string[] = ['']; - const testFiles: TestFile[] = []; - const parentNodes: { indent: number; item: TestFile | TestSuite }[] = []; - - const errorLine = /==*( *)ERRORS( *)=*/; - const errorFileLine = /__*( *)ERROR collecting (.*)/; - const lastLineWithErrors = /==*.*/; - - let haveErrors = false; - - let packagePrefix: string = ''; - content.split(/\r?\n/g).forEach((line, index, lines) => { - if (options.token && options.token.isCancellationRequested) { - return; - } - - const trimmedLine: string = line.trim(); - - if (trimmedLine.startsWith('<Package \'')) { - // Process the previous lines. - this.parsePyTestModuleCollectionResult(options.cwd, logOutputLines, testFiles, parentNodes, packagePrefix); - logOutputLines = ['']; - - packagePrefix = this.extractPackageName(trimmedLine, options.cwd); - } - - if (trimmedLine.startsWith('<Module \'') || index === lines.length - 1) { - // Process the previous lines. - this.parsePyTestModuleCollectionResult(options.cwd, logOutputLines, testFiles, parentNodes, packagePrefix); - logOutputLines = ['']; - } - if (errorLine.test(line)) { - haveErrors = true; - logOutputLines = ['']; - return; - } - if (errorFileLine.test(line)) { - haveErrors = true; - if (logOutputLines.length !== 1 && logOutputLines[0].length !== 0) { - this.parsePyTestModuleCollectionError(options.cwd, logOutputLines, testFiles, parentNodes); - logOutputLines = ['']; - } - } - if (lastLineWithErrors.test(line) && haveErrors) { - this.parsePyTestModuleCollectionError(options.cwd, logOutputLines, testFiles, parentNodes); - logOutputLines = ['']; - } - if (index === 0) { - if (content.startsWith(os.EOL) || lines.length > 1) { - logOutputLines[logOutputLines.length - 1] += line; - logOutputLines.push(''); - return; - } - logOutputLines[logOutputLines.length - 1] += line; - return; - } - if (index === lines.length - 1) { - logOutputLines[logOutputLines.length - 1] += line; - return; - } - logOutputLines[logOutputLines.length - 1] += line; - logOutputLines.push(''); - return; - }); - - return testFiles; - } - - private parsePyTestModuleCollectionError(rootDirectory: string, lines: string[], testFiles: TestFile[], - parentNodes: { indent: number; item: TestFile | TestSuite }[]) { - - lines = lines.filter(line => line.trim().length > 0); - if (lines.length <= 1) { - return; - } - - const errorFileLine = lines[0]; - let fileName = errorFileLine.substring(errorFileLine.indexOf('ERROR collecting') + 'ERROR collecting'.length).trim(); - fileName = fileName.substr(0, fileName.lastIndexOf(' ')); - - const currentPackage = convertFileToPackage(fileName); - const fullyQualifiedName = path.isAbsolute(fileName) ? fileName : path.resolve(rootDirectory, fileName); - const testFile = { - functions: [], suites: [], name: fileName, fullPath: fullyQualifiedName, - nameToRun: fileName, xmlName: currentPackage, time: 0, errorsWhenDiscovering: lines.join('\n') - }; - testFiles.push(testFile); - parentNodes.push({ indent: 0, item: testFile }); - - return; - - } - - /** - * Extract the 'package' name from a given PyTest (>= 3.7) output line. - * - * @param packageLine A single line of output from pytest that starts with `<Package` (may have leading white space). - * @param rootDir Value is pytest's `--rootdir=` parameter. - */ - private extractPackageName(packageLine: string, rootDir: string): string { - const packagePath: string = extractBetweenDelimiters(packageLine, DELIMITER, DELIMITER); - let packageName: string = path.normalize(packagePath); - const tmpRoot: string = path.normalize(rootDir); - - if (packageName.indexOf(tmpRoot) === 0) { - packageName = packageName.substring(tmpRoot.length); - if (packageName.startsWith(path.sep)) { - packageName = packageName.substring(1); - } - if (packageName.endsWith(path.sep)) { - packageName = packageName.substring(0, packageName.length - 1); - } - } - packageName = packageName.replace(/\\/g, '/'); - return packageName; - } - - private parsePyTestModuleCollectionResult( - rootDirectory: string, - lines: string[], - testFiles: TestFile[], - parentNodes: { indent: number; item: TestFile | TestSuite }[], - packagePrefix: string = '' - ) { - - let currentPackage: string = ''; - - lines.forEach(line => { - const trimmedLine = line.trim(); - let name: string = extractBetweenDelimiters(trimmedLine, DELIMITER, DELIMITER); - const indent = line.indexOf('<'); - - if (trimmedLine.startsWith('<Module \'')) { - if (packagePrefix && packagePrefix.length > 0) { - name = packagePrefix.concat('/', name); - } - currentPackage = convertFileToPackage(name); - const fullyQualifiedName = path.isAbsolute(name) ? name : path.resolve(rootDirectory, name); - const testFile = { - functions: [], suites: [], name: name, fullPath: fullyQualifiedName, - nameToRun: name, xmlName: currentPackage, time: 0 - }; - testFiles.push(testFile); - parentNodes.push({ indent: indent, item: testFile }); - return; - } - - const parentNode = this.findParentOfCurrentItem(indent, parentNodes); - - if (parentNode && trimmedLine.startsWith('<Class \'') || trimmedLine.startsWith('<UnitTestCase \'')) { - const isUnitTest = trimmedLine.startsWith('<UnitTestCase \''); - const rawName = `${parentNode!.item.nameToRun}::${name}`; - const xmlName = `${parentNode!.item.xmlName}.${name}`; - const testSuite: TestSuite = { name: name, nameToRun: rawName, functions: [], suites: [], isUnitTest: isUnitTest, isInstance: false, xmlName: xmlName, time: 0 }; - parentNode!.item.suites.push(testSuite); - parentNodes.push({ indent: indent, item: testSuite }); - return; - } - if (parentNode && trimmedLine.startsWith('<Instance \'')) { - // tslint:disable-next-line:prefer-type-cast - const suite = (parentNode!.item as TestSuite); - // suite.rawName = suite.rawName + '::()'; - // suite.xmlName = suite.xmlName + '.()'; - suite.isInstance = true; - return; - } - if (parentNode && trimmedLine.startsWith('<TestCaseFunction \'') || trimmedLine.startsWith('<Function \'')) { - const rawName = `${parentNode!.item.nameToRun}::${name}`; - const fn: TestFunction = { name: name, nameToRun: rawName, time: 0 }; - parentNode!.item.functions.push(fn); - return; - } - }); - } - - private findParentOfCurrentItem(indentOfCurrentItem: number, parentNodes: { indent: number; item: TestFile | TestSuite }[]): { indent: number; item: TestFile | TestSuite } | undefined { - while (parentNodes.length > 0) { - const parentNode = parentNodes[parentNodes.length - 1]; - if (parentNode.indent < indentOfCurrentItem) { - return parentNode; - } - parentNodes.pop(); - continue; - } - - return; - } -} - - /* Sample output from pytest --collect-only - <Module 'test_another.py'> - <Class 'Test_CheckMyApp'> - <Instance '()'> - <Function 'test_simple_check'> - <Function 'test_complex_check'> - <Module 'test_one.py'> - <UnitTestCase 'Test_test1'> - <TestCaseFunction 'test_A'> - <TestCaseFunction 'test_B'> - <Module 'test_two.py'> - <UnitTestCase 'Test_test1'> - <TestCaseFunction 'test_A2'> - <TestCaseFunction 'test_B2'> - <Module 'testPasswords/test_Pwd.py'> - <UnitTestCase 'Test_Pwd'> - <TestCaseFunction 'test_APwd'> - <TestCaseFunction 'test_BPwd'> - <Module 'testPasswords/test_multi.py'> - <Class 'Test_CheckMyApp'> - <Instance '()'> - <Function 'test_simple_check'> - <Function 'test_complex_check'> - <Class 'Test_NestedClassA'> - <Instance '()'> - <Function 'test_nested_class_methodB'> - <Class 'Test_nested_classB_Of_A'> - <Instance '()'> - <Function 'test_d'> - <Function 'test_username'> - <Function 'test_parametrized_username[one]'> - <Function 'test_parametrized_username[two]'> - <Function 'test_parametrized_username[three]'> - */ diff --git a/src/client/unittests/pytest/services/testMessageService.ts b/src/client/unittests/pytest/services/testMessageService.ts deleted file mode 100644 index f054851f3ed1..000000000000 --- a/src/client/unittests/pytest/services/testMessageService.ts +++ /dev/null @@ -1,253 +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 { 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, IPythonUnitTestMessage, PythonUnitTestMessageSeverity } 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 testResults Details about all known tests. - */ - public async getFilteredTestMessages(rootDirectory: string, testResults: Tests): Promise<IPythonUnitTestMessage[]> { - const testFuncs: FlattenedTestFunction[] = testResults.testFunctions.reduce((filtered, test) => { - if (test.testFunction.passed !== undefined || test.testFunction.status === TestStatus.Skipped){ - filtered.push(test); - } - return filtered; - }, []); - const messages: IPythonUnitTestMessage[] = []; - 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: IPythonUnitTestMessage = { - code: nameToRun, - severity: PythonUnitTestMessageSeverity.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 = PythonUnitTestMessageSeverity.Error; - if (tf.testFunction.status === TestStatus.Skipped) { - severity = PythonUnitTestMessageSeverity.Skip; - } - - const msg: IPythonUnitTestMessage = { - 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<ILocationStackFrameDetails[]> { - 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; - let lineNum = testFunction.testFunction.line; - let lineText: string; - let trimmedLineText: string; - const testDefPrefix = 'def '; - - while (testDefLine === undefined) { - const possibleTestDefLine = testSourceFile.lineAt(lineNum); - lineText = possibleTestDefLine.text; - trimmedLineText = lineText.trimLeft(); - if (trimmedLineText.toLowerCase().startsWith(testDefPrefix)) { - testDefLine = possibleTestDefLine; - } 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 testSimpleName = trimmedLineText.slice(testDefPrefix.length).match(/[^ \(:]+/)[0]; - const testDefStartCharNum = (lineText.length - trimmedLineText.length) + testDefPrefix.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>(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<ILocationStackFrameDetails> { - 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; - const suiteLocationStackFrameDetails: ILocationStackFrameDetails[] = []; - - const classPrefix = 'class '; - while (suiteStack.length > 0) { - let indentation: number; - let prevLowestIndentation: number; - // Get the name of the suite on top of the stack so it can be located. - const suiteName = suiteStack.shift(); - let suiteDefLineIndex: number; - 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 lineClassName = trimmedLineText.slice(classPrefix.length).match(/[^ \(:]+/)[0]; - - // 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/unittests/pytest/testConfigurationManager.ts b/src/client/unittests/pytest/testConfigurationManager.ts deleted file mode 100644 index 54b68718661c..000000000000 --- a/src/client/unittests/pytest/testConfigurationManager.ts +++ /dev/null @@ -1,54 +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'; - -export class ConfigurationManager extends TestConfigurationManager { - constructor(workspace: Uri, serviceContainer: IServiceContainer) { - super(workspace, Product.pytest, serviceContainer); - } - public async requiresUserToConfigure(wkspace: Uri): Promise<boolean> { - 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<string[]> { - const fs = this.serviceContainer.get<IFileSystem>(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/unittests/serviceRegistry.ts b/src/client/unittests/serviceRegistry.ts deleted file mode 100644 index 360853a59fc9..000000000000 --- a/src/client/unittests/serviceRegistry.ts +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { IServiceContainer, IServiceManager } from '../ioc/types'; -import { ArgumentsHelper } from './common/argumentsHelper'; -import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from './common/constants'; -import { DebugLauncher } from './common/debugLauncher'; -import { TestRunner } from './common/runner'; -import { TestConfigSettingsService } from './common/services/configSettingService'; -import { TestCollectionStorageService } from './common/services/storageService'; -import { TestManagerService } from './common/services/testManagerService'; -import { TestResultsService } from './common/services/testResultsService'; -import { UnitTestDiagnosticService } from './common/services/unitTestDiagnosticService'; -import { WorkspaceTestManagerService } from './common/services/workspaceTestManagerService'; -import { TestsHelper } from './common/testUtils'; -import { TestFlatteningVisitor } from './common/testVisitors/flatteningVisitor'; -import { TestFolderGenerationVisitor } from './common/testVisitors/folderGenerationVisitor'; -import { TestResultResetVisitor } from './common/testVisitors/resultResetVisitor'; -import { - ITestCollectionStorageService, ITestConfigSettingsService, ITestDebugLauncher, ITestDiscoveryService, ITestManager, ITestManagerFactory, ITestManagerService, ITestManagerServiceFactory, - ITestMessageService, ITestResultsService, ITestRunner, ITestsHelper, ITestsParser, ITestVisitor, IUnitTestSocketServer, IWorkspaceTestManagerService, IXUnitParser, TestProvider -} from './common/types'; -import { XUnitParser } from './common/xUnitParser'; -import { UnitTestConfigurationService } from './configuration'; -import { TestConfigurationManagerFactory } from './configurationFactory'; -import { TestResultDisplay } from './display/main'; -import { TestDisplay } from './display/picker'; -import { UnitTestManagementService } from './main'; -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 { TestsParser as PytestTestsParser } from './pytest/services/parserService'; -import { TestMessageService } from './pytest/services/testMessageService'; -import { IArgumentsHelper, IArgumentsService, ITestConfigurationManagerFactory, ITestDisplay, ITestManagerRunner, ITestResultDisplay, IUnitTestConfigurationService, IUnitTestDiagnosticService, IUnitTestHelper, IUnitTestManagementService } from './types'; -import { UnitTestHelper } from './unittest/helper'; -import { TestManager as UnitTestTestManager } from './unittest/main'; -import { TestManagerRunner as UnitTestTestManagerRunner } from './unittest/runner'; -import { ArgumentsService as UnitTestArgumentsService } from './unittest/services/argsService'; -import { TestDiscoveryService as UnitTestTestDiscoveryService } from './unittest/services/discoveryService'; -import { TestsParser as UnitTestTestsParser } from './unittest/services/parserService'; -import { UnitTestSocketServer } from './unittest/socketServer'; - -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton<ITestDebugLauncher>(ITestDebugLauncher, DebugLauncher); - serviceManager.addSingleton<ITestCollectionStorageService>(ITestCollectionStorageService, TestCollectionStorageService); - serviceManager.addSingleton<IWorkspaceTestManagerService>(IWorkspaceTestManagerService, WorkspaceTestManagerService); - - serviceManager.add<ITestsHelper>(ITestsHelper, TestsHelper); - serviceManager.add<IUnitTestSocketServer>(IUnitTestSocketServer, UnitTestSocketServer); - - serviceManager.add<ITestResultsService>(ITestResultsService, TestResultsService); - - serviceManager.add<ITestVisitor>(ITestVisitor, TestFlatteningVisitor, 'TestFlatteningVisitor'); - serviceManager.add<ITestVisitor>(ITestVisitor, TestFolderGenerationVisitor, 'TestFolderGenerationVisitor'); - serviceManager.add<ITestVisitor>(ITestVisitor, TestResultResetVisitor, 'TestResultResetVisitor'); - - serviceManager.add<ITestsParser>(ITestsParser, UnitTestTestsParser, UNITTEST_PROVIDER); - serviceManager.add<ITestsParser>(ITestsParser, PytestTestsParser, PYTEST_PROVIDER); - serviceManager.add<ITestsParser>(ITestsParser, NoseTestTestsParser, NOSETEST_PROVIDER); - - serviceManager.add<ITestDiscoveryService>(ITestDiscoveryService, UnitTestTestDiscoveryService, UNITTEST_PROVIDER); - serviceManager.add<ITestDiscoveryService>(ITestDiscoveryService, PytestTestDiscoveryService, PYTEST_PROVIDER); - serviceManager.add<ITestDiscoveryService>(ITestDiscoveryService, NoseTestDiscoveryService, NOSETEST_PROVIDER); - - serviceManager.add<IArgumentsHelper>(IArgumentsHelper, ArgumentsHelper); - serviceManager.add<ITestRunner>(ITestRunner, TestRunner); - serviceManager.add<IXUnitParser>(IXUnitParser, XUnitParser); - serviceManager.add<IUnitTestHelper>(IUnitTestHelper, UnitTestHelper); - - serviceManager.add<IArgumentsService>(IArgumentsService, PyTestArgumentsService, PYTEST_PROVIDER); - serviceManager.add<IArgumentsService>(IArgumentsService, NoseTestArgumentsService, NOSETEST_PROVIDER); - serviceManager.add<IArgumentsService>(IArgumentsService, UnitTestArgumentsService, UNITTEST_PROVIDER); - serviceManager.add<ITestManagerRunner>(ITestManagerRunner, PytestManagerRunner, PYTEST_PROVIDER); - serviceManager.add<ITestManagerRunner>(ITestManagerRunner, NoseTestManagerRunner, NOSETEST_PROVIDER); - serviceManager.add<ITestManagerRunner>(ITestManagerRunner, UnitTestTestManagerRunner, UNITTEST_PROVIDER); - - serviceManager.addSingleton<IUnitTestConfigurationService>(IUnitTestConfigurationService, UnitTestConfigurationService); - serviceManager.addSingleton<IUnitTestManagementService>(IUnitTestManagementService, UnitTestManagementService); - serviceManager.addSingleton<ITestResultDisplay>(ITestResultDisplay, TestResultDisplay); - serviceManager.addSingleton<ITestDisplay>(ITestDisplay, TestDisplay); - serviceManager.addSingleton<ITestConfigSettingsService>(ITestConfigSettingsService, TestConfigSettingsService); - serviceManager.addSingleton<ITestConfigurationManagerFactory>(ITestConfigurationManagerFactory, TestConfigurationManagerFactory); - - serviceManager.addSingleton<IUnitTestDiagnosticService>(IUnitTestDiagnosticService, UnitTestDiagnosticService); - serviceManager.addSingleton<ITestMessageService>(ITestMessageService, TestMessageService, PYTEST_PROVIDER); - - 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}'`); - } - } - }; - }); - - 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); - }; - }); -} diff --git a/src/client/unittests/types.ts b/src/client/unittests/types.ts deleted file mode 100644 index a0d00e3ba2ed..000000000000 --- a/src/client/unittests/types.ts +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { DiagnosticSeverity, Disposable, DocumentSymbolProvider, Event, Location, TextDocument, Uri } from 'vscode'; -import { Product } from '../common/types'; -import { CommandSource } from './common/constants'; -import { FlattenedTestFunction, ITestManager, ITestResultsService, TestFile, TestFunction, TestRunOptions, Tests, TestStatus, TestsToRun, UnitTestProduct } from './common/types'; - -export const IUnitTestConfigurationService = Symbol('IUnitTestConfigurationService'); -export interface IUnitTestConfigurationService { - displayTestFrameworkError(wkspace: Uri): Promise<void>; - selectTestRunner(placeHolderMessage: string): Promise<UnitTestProduct | undefined>; - enableTest(wkspace: Uri, product: UnitTestProduct); -} - -export const ITestResultDisplay = Symbol('ITestResultDisplay'); - -export interface ITestResultDisplay extends Disposable { - enabled: boolean; - readonly onDidChange: Event<void>; - displayProgressStatus(testRunResult: Promise<Tests>, debug?: boolean): void; - displayDiscoverStatus(testDiscovery: Promise<Tests>, quietMode?: boolean): Promise<Tests>; -} - -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<FlattenedTestFunction>; - selectTestFile(rootDirectory: string, tests: Tests): Promise<TestFile>; - displayFunctionTestPickerUI(cmdSource: CommandSource, wkspace: Uri, rootDirectory: string, file: Uri, testFunctions: TestFunction[], debug?: boolean): void; -} - -export const IUnitTestManagementService = Symbol('IUnitTestManagementService'); -export interface IUnitTestManagementService { - activate(): Promise<void>; - activateCodeLenses(symboldProvider: DocumentSymbolProvider): Promise<void>; - getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise<ITestManager | undefined | void>; - discoverTestsForDocument(doc: TextDocument): Promise<void>; - autoDiscoverTests(): Promise<void>; - discoverTests(cmdSource: CommandSource, resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean, quietMode?: boolean): Promise<void>; - stopTests(resource: Uri): Promise<void>; - displayStopUI(message: string): Promise<void>; - displayUI(cmdSource: CommandSource): Promise<void>; - displayPickerUI(cmdSource: CommandSource, file: Uri, testFunctions: TestFunction[], debug?: boolean): Promise<void>; - runTestsImpl(cmdSource: CommandSource, resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<void>; - runCurrentTestFile(cmdSource: CommandSource): Promise<void>; - - selectAndRunTestFile(cmdSource: CommandSource): Promise<void>; - - selectAndRunTestMethod(cmdSource: CommandSource, resource: Uri, debug?: boolean): Promise<void>; - - viewOutput(cmdSource: CommandSource): void; -} - -export interface ITestConfigurationManager { - requiresUserToConfigure(wkspace: Uri): Promise<boolean>; - configure(wkspace: Uri): Promise<void>; - enable(): Promise<void>; - disable(): Promise<void>; -} - -export const ITestConfigurationManagerFactory = Symbol('ITestConfigurationManagerFactory'); -export interface ITestConfigurationManagerFactory { - create(wkspace: Uri, product: Product): 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<Tests>; -} - -export const IUnitTestHelper = Symbol('IUnitTestHelper'); -export interface IUnitTestHelper { - getStartDirectory(args: string[]): string; - getIdsOfTestsToRun(tests: Tests, testsToRun: TestsToRun): string[]; -} - -export const IUnitTestDiagnosticService = Symbol('IUnitTestDiagnosticService'); -export interface IUnitTestDiagnosticService { - getMessagePrefix(status: TestStatus): string; - getSeverity(unitTestSeverity: PythonUnitTestMessageSeverity): DiagnosticSeverity; -} - -export interface IPythonUnitTestMessage { - code: string | undefined; - message?: string; - severity: PythonUnitTestMessageSeverity; - provider: string; - traceback?: string; - testTime: number; - status: TestStatus; - locationStack?: ILocationStackFrameDetails[]; - testFilePath: string; -} -export enum PythonUnitTestMessageSeverity { - Error, - Failure, - Skip, - Pass -} -export enum DiagnosticMessageType { - Error, - Fail, - Skipped, - Pass -} - -export interface ILocationStackFrameDetails { - location: Location; - lineText: string; -} diff --git a/src/client/unittests/unittest/helper.ts b/src/client/unittests/unittest/helper.ts deleted file mode 100644 index f89c4c287b39..000000000000 --- a/src/client/unittests/unittest/helper.ts +++ /dev/null @@ -1,52 +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>(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/unittests/unittest/main.ts b/src/client/unittests/unittest/main.ts deleted file mode 100644 index 8b6810bd56d4..000000000000 --- a/src/client/unittests/unittest/main.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Uri } from 'vscode'; -import { Product } from '../../common/types'; -import { noop } from '../../common/utils/misc'; -import { IServiceContainer } from '../../ioc/types'; -import { 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.unitTest.unittestEnabled; - } - constructor(workspaceFolder: Uri, rootDirectory: string, serviceContainer: IServiceContainer) { - super(UNITTEST_PROVIDER, Product.unittest, workspaceFolder, rootDirectory, serviceContainer); - this.argsService = this.serviceContainer.get<IArgumentsService>(IArgumentsService, this.testProvider); - this.helper = this.serviceContainer.get<ITestsHelper>(ITestsHelper); - this.runner = this.serviceContainer.get<ITestManagerRunner>(ITestManagerRunner, this.testProvider); - } - public configure() { - noop(); - } - public getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions { - const args = this.settings.unitTest.unittestArgs.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.unitTest.unittestArgs, runAllTests ? TestFilter.debugAll : TestFilter.debugSpecific); - } else { - args = this.argsService.filterArguments(this.settings.unitTest.unittestArgs, runAllTests ? TestFilter.runAll : TestFilter.runSpecific); - } - - if (runFailedTests === true) { - testsToRun = { testFile: [], testFolder: [], testSuite: [], testFunction: [] }; - testsToRun.testFunction = tests.testFunctions.filter(fn => { - return fn.testFunction.status === TestStatus.Error || fn.testFunction.status === TestStatus.Fail; - }).map(fn => fn.testFunction); - } - 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/unittests/unittest/runner.ts b/src/client/unittests/unittest/runner.ts deleted file mode 100644 index 9407def626e8..000000000000 --- a/src/client/unittests/unittest/runner.ts +++ /dev/null @@ -1,191 +0,0 @@ -'use strict'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../common/constants'; -import { ILogger } 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<string, TestStatusMap>(); -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 readonly logger: ILogger; - private busy!: Deferred<Tests>; - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.argsHelper = serviceContainer.get<IArgumentsHelper>(IArgumentsHelper); - this.testRunner = serviceContainer.get<ITestRunner>(ITestRunner); - this.server = this.serviceContainer.get<IUnitTestSocketServer>(IUnitTestSocketServer); - this.logger = this.serviceContainer.get<ILogger>(ILogger); - this.helper = this.serviceContainer.get<IUnitTestHelper>(IUnitTestHelper); - } - - // tslint:disable-next-line:max-func-body-length - public async runTest(testResultsService: ITestResultsService, options: TestRunOptions, testManager: ITestManager): Promise<Tests> { - if (this.busy && !this.busy.completed) { - return this.busy.promise; - } - this.busy = createDeferred<Tests>(); - - options.tests.summary.errors = 0; - options.tests.summary.failures = 0; - options.tests.summary.passed = 0; - options.tests.summary.skipped = 0; - let failFast = false; - const testLauncherFile = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'visualstudio_py_testlauncher.py'); - this.server.on('error', (message: string, ...data: string[]) => this.logger.logError(`${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; - 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>(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 runOptions: Options = { - args: [testLauncherFile].concat(testArgs), - 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 { - let promise = Promise.resolve<void>(undefined); - // Ok, the test runner can only work with one test at a time. - if (options.testsToRun) { - if (Array.isArray(options.testsToRun.testFile)) { - options.testsToRun.testFile.forEach(testFile => { - promise = promise.then(() => runTestInternal(testFile.fullPath, testFile.nameToRun)); - }); - } - if (Array.isArray(options.testsToRun.testSuite)) { - options.testsToRun.testSuite.forEach(testSuite => { - const testFileName = options.tests.testSuites.find(t => t.testSuite === testSuite)!.parentTestFile.fullPath; - promise = promise.then(() => runTestInternal(testFileName, testSuite.nameToRun)); - }); - } - if (Array.isArray(options.testsToRun.testFunction)) { - options.testsToRun.testFunction.forEach(testFn => { - const testFileName = options.tests.testFunctions.find(t => t.testFunction === testFn)!.parentTestFile.fullPath; - promise = promise.then(() => runTestInternal(testFileName, testFn.nameToRun)); - }); - } - - await this.removeListenersAfter(promise); - } - - } - - 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<any>): Promise<any> { - 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/unittests/unittest/services/argsService.ts b/src/client/unittests/unittest/services/argsService.ts deleted file mode 100644 index 26b530da23d7..000000000000 --- a/src/client/unittests/unittest/services/argsService.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 { 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>(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/unittests/unittest/services/discoveryService.ts b/src/client/unittests/unittest/services/discoveryService.ts deleted file mode 100644 index fdf28cff3c93..000000000000 --- a/src/client/unittests/unittest/services/discoveryService.ts +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable, named } from 'inversify'; -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>(IArgumentsHelper); - this.runner = serviceContainer.get<ITestRunner>(ITestRunner); - } - public async discoverTests(options: TestDiscoveryOptions): Promise<Tests> { - const pythonScript = this.getDiscoveryScript(options); - const unitTestOptions = this.translateOptions(options); - const runOptions: Options = { - args: ['-c', pythonScript], - 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<Tests>('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/unittests/unittest/services/parserService.ts b/src/client/unittests/unittest/services/parserService.ts deleted file mode 100644 index 6e99f1044d3a..000000000000 --- a/src/client/unittests/unittest/services/parserService.ts +++ /dev/null @@ -1,109 +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 { 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(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(rootDirectory: string, testIds: string[]): Tests { - const testFiles: TestFile[] = []; - testIds.forEach(testId => this.addTestId(rootDirectory, testId, testFiles)); - - return this.testsHelper.flattenTestFiles(testFiles); - } - - /** - * 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[]} testIds - * @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()!; - - // Check if we already have this test file - let testFile = testFiles.find(test => test.fullPath === filePath); - if (!testFile) { - testFile = { - name: path.basename(filePath), - fullPath: filePath, - functions: [], - suites: [], - nameToRun: `${suiteToRun}.${functionName}`, - 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 = { - name: className, - functions: [], - suites: [], - isUnitTest: true, - isInstance: false, - nameToRun: suiteToRun, - xmlName: '', - status: TestStatus.Idle, - time: 0 - }; - testFile.suites.push(testSuite!); - } - - const testFunction: TestFunction = { - name: functionName, - nameToRun: testId, - status: TestStatus.Idle, - time: 0 - }; - - testSuite!.functions.push(testFunction); - } -} diff --git a/src/client/unittests/unittest/socketServer.ts b/src/client/unittests/unittest/socketServer.ts deleted file mode 100644 index ca4535b6597f..000000000000 --- a/src/client/unittests/unittest/socketServer.ts +++ /dev/null @@ -1,121 +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-next-line:variable-name -const MaxConnections = 100; - -@injectable() -export class UnitTestSocketServer extends EventEmitter implements IUnitTestSocketServer { - private server?: net.Server; - private startedDef?: Deferred<number>; - 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<number> { - this.ipcBuffer = ''; - this.startedDef = createDeferred<number>(); - 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().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, ...data) { - 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/unittests/unittest/testConfigurationManager.ts b/src/client/unittests/unittest/testConfigurationManager.ts deleted file mode 100644 index 54416559ca30..000000000000 --- a/src/client/unittests/unittest/testConfigurationManager.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Uri } from 'vscode'; -import { Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { TestConfigurationManager } from '../common/managers/testConfigurationManager'; - -export class ConfigurationManager extends TestConfigurationManager { - constructor(workspace: Uri, serviceContainer: IServiceContainer) { - super(workspace, Product.unittest, serviceContainer); - } - public async requiresUserToConfigure(_wkspace: Uri): Promise<boolean> { - 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/workspaceSymbols/contracts.ts b/src/client/workspaceSymbols/contracts.ts deleted file mode 100644 index cb53f8b7d397..000000000000 --- a/src/client/workspaceSymbols/contracts.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Position, SymbolKind } from 'vscode'; - -export interface Tag { - 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 cb1453bbd181..000000000000 --- a/src/client/workspaceSymbols/generator.ts +++ /dev/null @@ -1,95 +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 { WORKSPACE_SYMBOLS_BUILD } 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<void> { - 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(WORKSPACE_SYMBOLS_BUILD) - private async generateTags(source: { directory?: string; file?: string }): Promise<void> { - 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<void>(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 38b7c1324a6e..000000000000 --- a/src/client/workspaceSymbols/main.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { CancellationToken, Disposable, languages, OutputChannel } from 'vscode'; -import { IApplicationShell, ICommandManager, 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; - - constructor(private serviceContainer: IServiceContainer) { - this.outputChannel = this.serviceContainer.get<OutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.commandMgr = this.serviceContainer.get<ICommandManager>(ICommandManager); - this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem); - this.workspace = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - this.processFactory = this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory); - this.appShell = this.serviceContainer.get<IApplicationShell>(IApplicationShell); - this.configurationService = this.serviceContainer.get<IConfigurationService>(IConfigurationService); - 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())); - } - 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 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); - })); - } - - // tslint:disable-next-line:no-any - private buildWorkspaceSymbols(rebuild: boolean = true, token?: CancellationToken): Promise<any>[] { - if (token && token.isCancellationRequested) { - return []; - } - if (this.generators.length === 0) { - return []; - } - - let promptPromise: Promise<InstallerResponse>; - 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)) { - this.outputChannel.show(); - 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>(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 162108e8ee1f..000000000000 --- a/src/client/workspaceSymbols/parser.ts +++ /dev/null @@ -1,171 +0,0 @@ -import * as path from 'path'; -import * as vscode from 'vscode'; -import { fsExistsAsync } from '../common/utils/fs'; -import { Tag } from './contracts'; - -// tslint:disable:no-require-imports no-var-requires no-suspicious-comment -// 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 = '(?<name>\\w+)\\t(?<file>.*)\\t\\/\\^(?<code>.*)\\$\\/;"\\tkind:(?<type>\\w+)\\tline:(?<line>\\d+)$'; - -export interface IRegexGroup { - name: string; - file: string; - code: string; - type: string; - line: number; -} - -export function matchNamedRegEx(data, regex): IRegexGroup | null { - const compiledRegexp = NamedRegexp(regex, 'g'); - const rawMatch = compiledRegexp.exec(data); - if (rawMatch !== null) { - return <IRegexGroup>rawMatch.groups(); - } - - return null; -} - -const CTagKinMapping = new Map<string, vscode.SymbolKind>(); -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[key.substring(1)] = value; -}); -Object.keys(newValuesAndKeys).forEach(key => { - CTagKinMapping.set(key, newValuesAndKeys[key]); -}); - -export function parseTags( - workspaceFolder: string, - tagFile: string, - query: string, - token: vscode.CancellationToken, - maxItems: number = 200 -): Promise<Tag[]> { - return fsExistsAsync(tagFile).then(exists => { - if (!exists) { - return Promise.resolve([]); - } - - return new Promise<Tag[]>((resolve, reject) => { - const lr = new LineByLineReader(tagFile); - let lineNumber = 0; - const tags: Tag[] = []; - - lr.on('error', (err) => { - reject(err); - }); - - lr.on('line', (line) => { - 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): Tag | 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 5c20d386feb1..000000000000 --- a/src/client/workspaceSymbols/provider.ts +++ /dev/null @@ -1,59 +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 { WORKSPACE_SYMBOLS_GO_TO } 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(WORKSPACE_SYMBOLS_GO_TO) - public async provideWorkspaceSymbols(query: string, token: CancellationToken): Promise<SymbolInformation[]> { - 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); - 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/history-react/MainPanel.tsx b/src/datascience-ui/history-react/MainPanel.tsx deleted file mode 100644 index 8d1b7ddbd7c7..000000000000 --- a/src/datascience-ui/history-react/MainPanel.tsx +++ /dev/null @@ -1,488 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './mainPanel.css'; - -import { min } from 'lodash'; -import * as React from 'react'; - -import { concatMultilineString } from '../../client/datascience/common'; -import { HistoryMessages } from '../../client/datascience/constants'; -import { CellState, ICell, IHistoryInfo } from '../../client/datascience/types'; -import { ErrorBoundary } from '../react-common/errorBoundary'; -import { getLocString } from '../react-common/locReactSide'; -import { IMessageHandler, PostOffice } from '../react-common/postOffice'; -import { Progress } from '../react-common/progress'; -import { Cell, ICellViewModel } from './cell'; -import { CellButton } from './cellButton'; -import { Image, ImageName } from './image'; -import { createCellVM, generateTestState, IMainPanelState } from './mainPanelState'; -import { MenuBar } from './menuBar'; - -export interface IMainPanelProps { - skipDefault?: boolean; - ignoreProgress? : boolean; - ignoreSysInfo? : boolean; - ignoreScrolling? : boolean; - theme: string; -} - -export class MainPanel extends React.Component<IMainPanelProps, IMainPanelState> implements IMessageHandler { - private stackLimit = 10; - - private bottom: HTMLDivElement | undefined; - - // tslint:disable-next-line:max-func-body-length - constructor(props: IMainPanelProps, state: IMainPanelState) { - super(props); - - // Default state should show a busy message - this.state = { cellVMs: [], busy: true, undoStack: [], redoStack : [] }; - - if (!this.props.skipDefault) { - this.state = generateTestState(this.inputBlockToggled); - } - } - - public componentDidMount() { - this.scrollToBottom(); - } - - public componentDidUpdate(prevProps, prevState) { - this.scrollToBottom(); - } - - public render() { - - const progressBar = this.state.busy && !this.props.ignoreProgress ? <Progress /> : undefined; - - return ( - <div className='main-panel'> - <PostOffice messageHandlers={[this]} /> - <MenuBar theme={this.props.theme} stylePosition='top-fixed'> - {this.renderExtraButtons()} - <CellButton theme={this.props.theme} onClick={this.collapseAll} disabled={!this.canCollapseAll()} tooltip={getLocString('DataScience.collapseAll', 'Collapse all cell inputs')}> - <Image theme={this.props.theme} class='cell-button-image' image={ImageName.CollapseAll}/> - </CellButton> - <CellButton theme={this.props.theme} onClick={this.expandAll} disabled={!this.canExpandAll()} tooltip={getLocString('DataScience.expandAll', 'Expand all cell inputs')}> - <Image theme={this.props.theme} class='cell-button-image' image={ImageName.ExpandAll}/> - </CellButton> - <CellButton theme={this.props.theme} onClick={this.export} disabled={!this.canExport()} tooltip={getLocString('DataScience.export', 'Export as Jupyter Notebook')}> - <Image theme={this.props.theme} class='cell-button-image' image={ImageName.SaveAs}/> - </CellButton> - <CellButton theme={this.props.theme} onClick={this.restartKernel} tooltip={getLocString('DataScience.restartServer', 'Restart iPython Kernel')}> - <Image theme={this.props.theme} class='cell-button-image' image={ImageName.Restart}/> - </CellButton> - <CellButton theme={this.props.theme} onClick={this.interruptKernel} tooltip={getLocString('DataScience.interruptKernel', 'Interrupt iPython Kernel')}> - <Image theme={this.props.theme} class='cell-button-image' image={ImageName.Interrupt}/> - </CellButton> - <CellButton theme={this.props.theme} onClick={this.undo} disabled={!this.canUndo()} tooltip={getLocString('DataScience.undo', 'Undo')}> - <Image theme={this.props.theme} class='cell-button-image' image={ImageName.Undo}/> - </CellButton> - <CellButton theme={this.props.theme} onClick={this.redo} disabled={!this.canRedo()} tooltip={getLocString('DataScience.redo', 'Redo')}> - <Image theme={this.props.theme} class='cell-button-image' image={ImageName.Redo}/> - </CellButton> - <CellButton theme={this.props.theme} onClick={this.clearAll} tooltip={getLocString('DataScience.clearAll', 'Remove All Cells')}> - <Image theme={this.props.theme} class='cell-button-image' image={ImageName.Cancel}/> - </CellButton> - </MenuBar> - <div className='top-spacing'/> - {progressBar} - {this.renderCells()} - <div ref={this.updateBottom}/> - </div> - ); - } - - // tslint:disable-next-line:no-any - public handleMessage = (msg: string, payload?: any) => { - switch (msg) { - case HistoryMessages.StartCell: - this.addCell(payload); - return true; - - case HistoryMessages.FinishCell: - this.finishCell(payload); - return true; - - case HistoryMessages.UpdateCell: - this.updateCell(payload); - return true; - - case HistoryMessages.GetAllCells: - this.getAllCells(); - return true; - - case HistoryMessages.ExpandAll: - this.expandAllSilent(); - return true; - - case HistoryMessages.CollapseAll: - this.collapseAllSilent(); - return true; - - case HistoryMessages.DeleteAllCells: - this.clearAllSilent(); - return true; - - case HistoryMessages.Redo: - this.redo(); - return true; - - case HistoryMessages.Undo: - this.undo(); - return true; - - case HistoryMessages.StartProgress: - if (!this.props.ignoreProgress) { - this.setState({busy: true}); - } - break; - - case HistoryMessages.StopProgress: - if (!this.props.ignoreProgress) { - this.setState({busy: false}); - } - break; - - default: - break; - } - - return false; - } - - private getAllCells = () => { - // Send all of our cells back to the other side - const cells = this.state.cellVMs.map((cellVM : ICellViewModel) => { - return cellVM.cell; - }) ; - - PostOffice.sendMessage({type: HistoryMessages.ReturnAllCells, payload: { contents: cells }}); - } - - private renderExtraButtons = () => { - if (!this.props.skipDefault) { - return <CellButton theme={this.props.theme} onClick={this.addMarkdown} tooltip='Add Markdown Test'>M</CellButton>; - } - - return null; - } - - private renderCells = () => { - return this.state.cellVMs.map((cellVM: ICellViewModel, index: number) => - <ErrorBoundary key={index}> - <Cell - cellVM={cellVM} - theme={this.props.theme} - gotoCode={() => this.gotoCellCode(index)} - delete={() => this.deleteCell(index)}/> - </ErrorBoundary> - ); - } - - private addMarkdown = () => { - this.addCell({ - data : { - cell_type: 'markdown', - metadata: {}, - source: [ - '## Cell 3\n', - 'Here\'s some markdown\n', - '- A List\n', - '- Of Items' - ] - }, - id : '1111', - file : 'foo.py', - line : 0, - state : CellState.finished - }); - } - - private collapseAll = () => { - PostOffice.sendMessage({ type: HistoryMessages.CollapseAll, payload: { }}); - this.collapseAllSilent(); - } - - private collapseAllSilent = () => { - const newCells = this.state.cellVMs.map((value: ICellViewModel) => { - if (value.inputBlockOpen) { - return this.toggleCellVM(value); - } else { - return {...value}; - } - }); - - // Now assign our new array copy to state - this.setState({ - cellVMs: newCells, - skipNextScroll: true - }); - } - - private expandAll = () => { - PostOffice.sendMessage({ type: HistoryMessages.ExpandAll, payload: { }}); - this.expandAllSilent(); - } - - private expandAllSilent = () => { - const newCells = this.state.cellVMs.map((value: ICellViewModel) => { - if (!value.inputBlockOpen) { - return this.toggleCellVM(value); - } else { - return {...value}; - } - }); - - // Now assign our new array copy to state - this.setState({ - cellVMs: newCells, - skipNextScroll: true - }); - } - - private canCollapseAll = () => { - return this.state.cellVMs.length > 0; - } - - private canExpandAll = () => { - return this.state.cellVMs.length > 0; - } - - private canExport = () => { - return this.state.cellVMs.length > 0 ; - } - - private canRedo = () => { - return this.state.redoStack.length > 0 ; - } - - private canUndo = () => { - return this.state.undoStack.length > 0 ; - } - - private pushStack = (stack : ICellViewModel[][], cells : ICellViewModel[]) => { - // Get the undo stack up to the maximum length - const slicedUndo = stack.slice(0, min([stack.length, this.stackLimit])); - - // Combine this with our set of cells - return [...slicedUndo, cells]; - } - - private gotoCellCode = (index: number) => { - // Find our cell - const cellVM = this.state.cellVMs[index]; - - // Send a message to the other side to jump to a particular cell - PostOffice.sendMessage({ type: HistoryMessages.GotoCodeCell, payload: { file : cellVM.cell.file, line: cellVM.cell.line }}); - } - - private deleteCell = (index: number) => { - PostOffice.sendMessage({ type: HistoryMessages.DeleteCell, payload: { }}); - - // Update our state - this.setState({ - cellVMs: this.state.cellVMs.filter((c : ICellViewModel, i: number) => { - return i !== index; - }), - undoStack : this.pushStack(this.state.undoStack, this.state.cellVMs), - skipNextScroll: true - }); - } - - private clearAll = () => { - PostOffice.sendMessage({ type: HistoryMessages.DeleteAllCells, payload: { }}); - this.clearAllSilent(); - } - - private clearAllSilent = () => { - // Update our state - this.setState({ - cellVMs: [], - undoStack : this.pushStack(this.state.undoStack, this.state.cellVMs), - skipNextScroll: true, - busy: false // No more progress on delete all - }); - - // Tell other side, we changed our number of cells - this.sendInfo(); - } - - private redo = () => { - // Pop one off of our redo stack and update our undo - const cells = this.state.redoStack[this.state.redoStack.length - 1]; - const redoStack = this.state.redoStack.slice(0, this.state.redoStack.length - 1); - const undoStack = this.pushStack(this.state.undoStack, this.state.cellVMs); - PostOffice.sendMessage({ type: HistoryMessages.Redo, payload: { }}); - this.setState({ - cellVMs: cells, - undoStack: undoStack, - redoStack: redoStack, - skipNextScroll: true - }); - - // Tell other side, we changed our number of cells - this.sendInfo(); - } - - private undo = () => { - // Pop one off of our undo stack and update our redo - const cells = this.state.undoStack[this.state.undoStack.length - 1]; - const undoStack = this.state.undoStack.slice(0, this.state.undoStack.length - 1); - const redoStack = this.pushStack(this.state.redoStack, this.state.cellVMs); - PostOffice.sendMessage({ type: HistoryMessages.Undo, payload: { }}); - this.setState({ - cellVMs: cells, - undoStack : undoStack, - redoStack : redoStack, - skipNextScroll : true - }); - - // Tell other side, we changed our number of cells - this.sendInfo(); - } - - private restartKernel = () => { - // Send a message to the other side to restart the kernel - PostOffice.sendMessage({ type: HistoryMessages.RestartKernel, payload: { }}); - } - - private interruptKernel = () => { - // Send a message to the other side to restart the kernel - PostOffice.sendMessage({ type: HistoryMessages.Interrupt, payload: { }}); - } - - private export = () => { - // Send a message to the other side to export our current list - const cellContents: ICell[] = this.state.cellVMs.map((cellVM: ICellViewModel, index: number) => { return cellVM.cell; }); - PostOffice.sendMessage({ type: HistoryMessages.Export, payload: { contents: cellContents }}); - } - - private scrollToBottom = () => { - if (this.bottom && this.bottom.scrollIntoView && !this.state.skipNextScroll && !this.props.ignoreScrolling) { - // Delay this until we are about to render. React hasn't setup the size of the bottom element - // yet so we need to delay. 10ms looks good from a user point of view - setTimeout(() => { - if (this.bottom) { - this.bottom.scrollIntoView({behavior: 'smooth', block : 'end', inline: 'end'}); - } - }, 100); - } - } - - private updateBottom = (newBottom: HTMLDivElement) => { - if (newBottom !== this.bottom) { - this.bottom = newBottom; - } - } - - // tslint:disable-next-line:no-any - private addCell = (payload?: any) => { - if (payload) { - const cell = payload as ICell; - const cellVM: ICellViewModel = createCellVM(cell, this.inputBlockToggled); - if (cellVM) { - this.setState({ - cellVMs: [...this.state.cellVMs, cellVM], - undoStack : this.pushStack(this.state.undoStack, this.state.cellVMs), - redoStack: this.state.redoStack, - skipNextScroll: false - }); - - // Tell other side, we changed our number of cells - this.sendInfo(); - } - } - } - - private inputBlockToggled = (id: string) => { - // Create a shallow copy of the array, let not const as this is the shallow array copy that we will be changing - const cellVMArray: ICellViewModel[] = [...this.state.cellVMs]; - const cellVMIndex = cellVMArray.findIndex((value: ICellViewModel) => { - return value.cell.id === id; - }); - - if (cellVMIndex >= 0) { - // Const here as this is the state object pulled off of our shallow array copy, we don't want to mutate it - const targetCellVM = cellVMArray[cellVMIndex]; - - // Mutate the shallow array copy - cellVMArray[cellVMIndex] = this.toggleCellVM(targetCellVM); - - this.setState({ - skipNextScroll: true, - cellVMs: cellVMArray - }); - } - } - - // Toggle the input collapse state of a cell view model return a shallow copy with updated values - private toggleCellVM = (cellVM: ICellViewModel) => { - let newCollapseState = cellVM.inputBlockOpen; - let newText = cellVM.inputBlockText; - - if (cellVM.cell.data.cell_type === 'code') { - newCollapseState = !newCollapseState; - newText = this.extractInputText(cellVM.cell); - if (!newCollapseState) { - if (newText.length > 0) { - newText = newText.split('\n', 1)[0]; - newText = newText.slice(0, 255); // Slice to limit length of string, slicing past the string length is fine - newText = newText.concat('...'); - } - } - } - - return {...cellVM, inputBlockOpen: newCollapseState, inputBlockText: newText}; - } - - private extractInputText = (cell: ICell) => { - return concatMultilineString(cell.data.source); - } - - private sendInfo = () => { - const info : IHistoryInfo = { - cellCount: this.state.cellVMs.length, - undoCount: this.state.undoStack.length, - redoCount: this.state.redoStack.length - }; - PostOffice.sendMessage({type: HistoryMessages.SendInfo, payload: { info: info }}); - } - - private updateOrAdd = (cell: ICell, allowAdd? : boolean) => { - const index = this.state.cellVMs.findIndex((c : ICellViewModel) => c.cell.id === cell.id); - if (index >= 0) { - // Update this cell - this.state.cellVMs[index].cell = cell; - this.forceUpdate(); - } else if (allowAdd) { - // This is an entirely new cell (it may have started out as finished) - this.addCell(cell); - } - } - - private isCellSupported(cell: ICell) : boolean { - return !this.props.ignoreSysInfo || cell.data.cell_type !== 'sys_info'; - } - - // tslint:disable-next-line:no-any - private finishCell = (payload?: any) => { - if (payload) { - const cell = payload as ICell; - if (cell && this.isCellSupported(cell)) { - this.updateOrAdd(cell, true); - } - } - } - - // tslint:disable-next-line:no-any - private updateCell = (payload?: any) => { - if (payload) { - const cell = payload as ICell; - if (cell && this.isCellSupported(cell)) { - this.updateOrAdd(cell, false); - } - } - } -} diff --git a/src/datascience-ui/history-react/cell.css b/src/datascience-ui/history-react/cell.css deleted file mode 100644 index 39a9ace2dd36..000000000000 --- a/src/datascience-ui/history-react/cell.css +++ /dev/null @@ -1,113 +0,0 @@ -.cell-wrapper { - margin: 10px; - padding: 2px; - display: block; - border-bottom-color: var(--vscode-editorGroupHeader-tabsBackground); - border-bottom-style: solid; - border-bottom-width: 1px; -} - -.cell-wrapper:after { - content: ""; - clear: both; - display: block; -} - -.cell-outer { - display:flex; - flex-direction: row; - justify-content: left; - width: 100%; -} - -.content-div { - float: left; - width: 100%; -} - -.controls-div { - float: left; -} - -.hide { - display: none; -} - -.cell-result-container { - width: 100%; -} - -.cell-input { - margin: 0; -} - -.cell-input pre{ - margin: 0px; - padding: 0px; -} - -.cell-output { - margin-top: 5px; - background: var(--vscode-notifications-background); - white-space: pre-wrap; - font-family: monospace; - max-height: 300px; - overflow-y: auto; - overflow-x: auto; -} - -.cell-output pre { - white-space: pre-wrap; - font-family: monospace; -} - -.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(--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(--vscode-editor-background); /* Force to white because the default color for output is gray */ -} -.cell-output tbody tr:hover { - background: var(--vscode-editor-selectionBackground); -} -.cell-output * + table { - margin-top: 1em; -} - -.controls-flex { - display:flex; - min-width: 40px; -} - -.center-img { - display: block; - margin: 0 auto; -} - diff --git a/src/datascience-ui/history-react/cell.tsx b/src/datascience-ui/history-react/cell.tsx deleted file mode 100644 index a5ce0c30f47f..000000000000 --- a/src/datascience-ui/history-react/cell.tsx +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './cell.css'; - -import { nbformat } from '@jupyterlab/coreutils'; -import ansiToHtml from 'ansi-to-html'; -import * as React from 'react'; -// tslint:disable-next-line:match-default-export-name import-name -import JSONTree from 'react-json-tree'; - -import { concatMultilineString, formatStreamText } from '../../client/datascience/common'; -import { CellState, ICell } from '../../client/datascience/types'; -import { noop } from '../../test/core'; -import { getLocString } from '../react-common/locReactSide'; -import { CellButton } from './cellButton'; -import { Code } from './code'; -import { CollapseButton } from './collapseButton'; -import { ExecutionCount } from './executionCount'; -import { Image, ImageName } from './image'; -import { MenuBar } from './menuBar'; -import { SysInfo } from './sysInfo'; -import { displayOrder, richestMimetype, transforms } from './transforms'; - -interface ICellProps { - cellVM: ICellViewModel; - theme: string; - gotoCode(): void; - delete(): void; -} - -export interface ICellViewModel { - cell: ICell; - inputBlockOpen: boolean; - inputBlockText: string; - inputBlockCollapseNeeded: boolean; - inputBlockToggled(id: string): void; -} - -export class Cell extends React.Component<ICellProps> { - constructor(prop: ICellProps) { - super(prop); - } - - public render() { - if (this.props.cellVM.cell.data.cell_type === 'sys_info') { - return <SysInfo theme={this.props.theme} connection={this.props.cellVM.cell.data.connection} path={this.props.cellVM.cell.data.path} message={this.props.cellVM.cell.data.message} version={this.props.cellVM.cell.data.version} notebook_version={this.props.cellVM.cell.data.notebook_version}/>; - } else { - return this.renderNormalCell(); - } - } - - // Public for testing - public getUnknownMimeTypeFormatString = () => { - return getLocString('DataScience.unknownMimeTypeFormat', 'Unknown Mime Type'); - } - - private toggleInputBlock = () => { - const cellId: string = this.getCell().id; - this.props.cellVM.inputBlockToggled(cellId); - } - - private getDeleteString = () => { - return getLocString('DataScience.deleteButtonTooltip', 'Remove Cell'); - } - - private getGoToCodeString = () => { - return getLocString('DataScience.gotoCodeButtonTooltip', 'Go to code'); - } - - 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 renderNormalCell() { - - return ( - <div className='cell-wrapper'> - <MenuBar theme={this.props.theme}> - <CellButton theme={this.props.theme} onClick={this.props.delete} tooltip={this.getDeleteString()}> - <Image theme={this.props.theme} class='cell-button-image' image={ImageName.Cancel}/> - </CellButton> - <CellButton theme={this.props.theme} onClick={this.props.gotoCode} tooltip={this.getGoToCodeString()}> - <Image theme={this.props.theme} class='cell-button-image' image={ImageName.GoToSourceCode}/> - </CellButton> - </MenuBar> - <div className='cell-outer'> - <div className='controls-div'> - <div className='controls-flex'> - <ExecutionCount cell={this.props.cellVM.cell} theme={this.props.theme} visible={this.isCodeCell()}/> - <CollapseButton theme={this.props.theme} hidden={this.props.cellVM.inputBlockCollapseNeeded} - open={this.props.cellVM.inputBlockOpen} onClick={this.toggleInputBlock} - tooltip={getLocString('DataScience.collapseInputTooltip', 'Collapse input block')}/> - </div> - </div> - <div className='content-div'> - <div className='cell-result-container'> - {this.renderInputs()} - {this.renderResults()} - </div> - </div> - </div> - </div> - ); - } - - private renderInputs = () => { - if (this.isCodeCell()) { - // Colorize our text - return (<div className='cell-input'><Code code={this.props.cellVM.inputBlockText} theme={this.props.theme}/></div>); - } else { - return null; - } - } - - private renderResults = () => { - const outputClassNames = this.isCodeCell() ? - `cell-output cell-output-${this.props.theme}` : - ''; - - // Results depend upon the type of cell - const results = this.isCodeCell() ? - this.renderCodeOutputs() : - this.renderMarkdown(this.getMarkdownCell()); - - // Then combine them inside a div - return <div className={outputClassNames}>{results}</div>; - } - private renderCodeOutputs = () => { - if (this.isCodeCell() && this.hasOutput()) { - // Render the outputs - return this.getCodeCell().outputs.map((output: nbformat.IOutput, index: number) => { - return this.renderOutput(output, index); - }); - - } - } - - private renderMarkdown = (markdown : nbformat.IMarkdownCell) => { - // React-markdown expects that the source is a string - const source = concatMultilineString(markdown.source); - const Transform = transforms['text/markdown']; - - return <Transform data={source}/>; - } - - private renderWithTransform = (mimetype: string, output : nbformat.IOutput, index : number) => { - - // If we found a mimetype, use the transform - if (mimetype) { - - // Get the matching React.Component for that mimetype - const Transform = transforms[mimetype]; - - if (typeof mimetype !== 'string') { - return <div key={index}>{this.getUnknownMimeTypeFormatString().format(mimetype)}</div>; - } - - try { - // Text/plain has to be massaged. It expects a continuous string - if (output.data) { - let data = output.data[mimetype]; - if (mimetype === 'text/plain') { - data = concatMultilineString(data); - } - - // Return the transformed control using the data we massaged - return <Transform key={index} data={data} />; - } - } catch (ex) { - window.console.log('Error in rendering'); - window.console.log(ex); - return <div></div>; - } - } - - return <div></div>; - } - - private renderOutput = (output : nbformat.IOutput, index: number) => { - // Borrowed this from Don's Jupyter extension - - // First make sure we have the mime data - if (!output) { - return <div key={index}/>; - } - - // Make a copy of our data so we don't modify our cell - const copy = {...output}; - - // Special case for json - if (copy.data && copy.data['application/json']) { - return <JSONTree key={index} data={copy.data} />; - } - - // Stream and error output need to be converted - if (copy.output_type === 'stream') { - // Stream output needs to be wrapped in xmp so it - // show literally. Otherwise < chars start a new html element. - const stream = copy as nbformat.IStream; - const multiline = concatMultilineString(stream.text); - const formatted = formatStreamText(multiline); - copy.data = { - 'text/html' : `<xmp>${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 (formatted.includes('<')) { - const converter = new ansiToHtml(); - const html = converter.toHtml(formatted); - copy.data = { - 'text/html': html - }; - } - } catch { - noop(); - } - - } else if (copy.output_type === 'error') { - const error = copy as nbformat.IError; - try { - const converter = new ansiToHtml(); - const trace = converter.toHtml(error.traceback.join('\n')); - copy.data = { - 'text/html': trace - }; - } catch { - // This can fail during unit tests, just use the raw data - copy.data = { - 'text/html': error.evalue - }; - - } - } - - // Jupyter style MIME bundle - - // Find out which mimetype is the richest - const mimetype: string = richestMimetype(copy.data, displayOrder, transforms); - - // If that worked, use the transform - if (mimetype) { - return this.renderWithTransform(mimetype, copy, index); - } - - const str : string = this.getUnknownMimeTypeFormatString().format(mimetype); - return <div key={index}>${str}</div>; - } -} diff --git a/src/datascience-ui/history-react/cellButton.css b/src/datascience-ui/history-react/cellButton.css deleted file mode 100644 index 64584d8bfdf2..000000000000 --- a/src/datascience-ui/history-react/cellButton.css +++ /dev/null @@ -1,44 +0,0 @@ -:root { - --button-size: 18px; -} - - -.cell-button { - border-width: 0px; - border-style: solid; - cursor: pointer; - text-align: center; - line-height: 16px; - overflow: hidden; - width: var(--button-size); - height: var(--button-size); - margin-left: 10px; - padding: 1px; - background-color: transparent; - cursor: hand; -} - -.cell-button-inner-disabled-filter { - opacity: 0.5; -} - -.cell-button-child { - max-width: 100%; - max-height: 100%; -} - -.cell-button-child img{ - max-width: 100%; - max-height: 100%; -} - -.cell-button-vscode-light:disabled { - border-color: gray; - filter: grayscale(100%); -} - -.cell-button-vscode-dark:disabled { - border-color: gray; - filter: grayscale(100%); -} - diff --git a/src/datascience-ui/history-react/cellButton.tsx b/src/datascience-ui/history-react/cellButton.tsx deleted file mode 100644 index 19cd73be3d34..000000000000 --- a/src/datascience-ui/history-react/cellButton.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; -import * as React from 'react'; -import './cellButton.css'; - -interface ICellButtonProps { - theme: string; - tooltip : string; - disabled?: boolean; - onClick() : void; -} - -export class CellButton extends React.Component<ICellButtonProps> { - constructor(props) { - super(props); - } - - public render() { - const classNames = `cell-button cell-button-${this.props.theme}`; - const innerFilter = this.props.disabled ? 'cell-button-inner-disabled-filter' : ''; - - return ( - <button role='button' aria-pressed='false' disabled={this.props.disabled} title={this.props.tooltip} className={classNames} onClick={this.props.onClick}> - <div className={innerFilter} > - <div className='cell-button-child'> - {this.props.children} - </div> - </div> - </button> - ); - } - -} diff --git a/src/datascience-ui/history-react/code.tsx b/src/datascience-ui/history-react/code.tsx deleted file mode 100644 index 5db6f5cfdf2e..000000000000 --- a/src/datascience-ui/history-react/code.tsx +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as Prism from 'prismjs'; -import * as React from 'react'; -import { transforms } from './transforms'; - -// Borrowed this from the prism stuff. Simpler than trying to -// get loadLanguages to behave with webpack. Does mean we might get out of date though. -const pythonGrammar = { - // tslint:disable-next-line:object-literal-key-quotes - 'comment': { - pattern: /(^|[^\\])#.*/, - lookbehind: true - }, - // tslint:disable-next-line:object-literal-key-quotes - 'triple-quoted-string': { - pattern: /("""|''')[\s\S]+?\1/, - greedy: true, - alias: 'string' - }, - // tslint:disable-next-line:object-literal-key-quotes - 'string': { - pattern: /("|')(?:\\.|(?!\1)[^\\\r\n])*\1/, - greedy: true - }, - // tslint:disable-next-line:object-literal-key-quotes - 'function': { - pattern: /((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g, - lookbehind: true - }, - // tslint:disable-next-line:object-literal-key-quotes - 'class-name': { - pattern: /(\bclass\s+)\w+/i, - lookbehind: true - }, - // tslint:disable-next-line:object-literal-key-quotes - 'keyword': /\b(?:as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|nonlocal|pass|print|raise|return|try|while|with|yield)\b/, - // tslint:disable-next-line:object-literal-key-quotes - 'builtin': /\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/, - // tslint:disable-next-line:object-literal-key-quotes - 'boolean': /\b(?:True|False|None)\b/, - // tslint:disable-next-line:object-literal-key-quotes - 'number': /(?:\b(?=\d)|\B(?=\.))(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i, - // tslint:disable-next-line:object-literal-key-quotes - 'operator': /[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]|\b(?:or|and|not)\b/, - // tslint:disable-next-line:object-literal-key-quotes - 'punctuation': /[{}[\];(),.:]/ -}; - -export interface ICodeProps { - code : string; - theme: string; -} - -export class Code extends React.Component<ICodeProps> { - constructor(prop: ICodeProps) { - super(prop); - } - - public render() { - const colorized = Prism.highlight(this.props.code, pythonGrammar); - const Transform = transforms['text/html']; - return (<pre><code className='language-python'><Transform data={colorized}/></code></pre>); - } -} diff --git a/src/datascience-ui/history-react/collapseButton.css b/src/datascience-ui/history-react/collapseButton.css deleted file mode 100644 index 78aaeaf21b65..000000000000 --- a/src/datascience-ui/history-react/collapseButton.css +++ /dev/null @@ -1,17 +0,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; -} - -.remove-style { - background-color:transparent; - border:transparent; -} diff --git a/src/datascience-ui/history-react/collapseButton.tsx b/src/datascience-ui/history-react/collapseButton.tsx deleted file mode 100644 index 00a19f8b406f..000000000000 --- a/src/datascience-ui/history-react/collapseButton.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'; -import './collapseButton.css'; - -interface ICollapseButtonProps { - theme: string; - tooltip: string; - hidden: boolean; - open: boolean; - onClick(): void; -} - -export class CollapseButton extends React.Component<ICollapseButtonProps> { - constructor(props) { - 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.hidden ? '' : ' hide'}`; - return ( - <div > - <button className={collapseInputClassNames} onClick={this.props.onClick}> - <svg version='1.1' baseProfile='full' width='8px' height='11px'> - <polygon points='0,0 0,10 5,5' className={collapseInputPolygonClassNames} fill='black' /> - </svg> - </button> - </div>); - } - -} diff --git a/src/datascience-ui/history-react/executionCount.css b/src/datascience-ui/history-react/executionCount.css deleted file mode 100644 index 94d4ba816970..000000000000 --- a/src/datascience-ui/history-react/executionCount.css +++ /dev/null @@ -1,37 +0,0 @@ -.execution-count { - font-weight: bold; - color: var(--comment-color); - } - - .execution-count-busy-outer { - font-weight: bold; - color: var(--comment-color); - display:flex; - width: 16px; - height: 16px; -} - .execution-count-busy-svg { - animation-name: spin; - animation-duration: 4000ms; - animation-iteration-count: infinite; - animation-timing-function: linear; - transform-origin: 50% 50%; - width: 16px; - height: 16px; -} - -.execution-count-busy-polyline { - fill: none; - stroke: var(--comment-color); - stroke-width: 5; -} - - @keyframes spin { - from { - transform:rotate(0deg); - } - to { - transform:rotate(360deg); - } -} - diff --git a/src/datascience-ui/history-react/executionCount.tsx b/src/datascience-ui/history-react/executionCount.tsx deleted file mode 100644 index 26782074f776..000000000000 --- a/src/datascience-ui/history-react/executionCount.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'; -import { CellState, ICell } from '../../client/datascience/types'; -import './executionCount.css'; - -interface IExecutionCountProps { - cell: ICell; - theme: string; - visible: boolean; -} - -export class ExecutionCount extends React.Component<IExecutionCountProps> { - constructor(props) { - super(props); - } - - public render() { - const isBusy = this.props.cell.state === CellState.init || this.props.cell.state === CellState.executing; - if (this.props.visible) { - - return isBusy ? - ( - <div className='execution-count-busy-outer'>[<svg className='execution-count-busy-svg' viewBox='0 0 100 100'><polyline points='50,0, 50,50, 85,15, 50,50, 100,50, 50,50, 85,85, 50,50 50,100 50,50 15,85 50,50 0,50 50,50 15,15' className='execution-count-busy-polyline' /></svg>]</div> - ) : - ( - <div className='execution-count'>{`[${this.props.cell.data.execution_count}]`}</div> - ); - } else { - return null; - } - } - -} diff --git a/src/datascience-ui/history-react/image.tsx b/src/datascience-ui/history-react/image.tsx deleted file mode 100644 index 01c46b0ddbfb..000000000000 --- a/src/datascience-ui/history-react/image.tsx +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as React from 'react'; -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, - PopIn, - PopOut, - Redo, - Restart, - SaveAs, - Undo -} - -// All of the images must be 'require' so that webpack doesn't rewrite the import as requiring a .default. -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'), - }, - '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'), - }, - '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'), - }, -} - -interface IImageProps { - theme: string; - image: ImageName; - class: string; -} - -export class Image extends React.Component<IImageProps> { - constructor(props) { - 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.theme.includes('dark') ? image.dark : image.light; - return ( - <InlineSVG className={this.props.class} src={source}/> - ) - } - -} diff --git a/src/datascience-ui/history-react/images.d.ts b/src/datascience-ui/history-react/images.d.ts deleted file mode 100644 index f83b33cbc711..000000000000 --- a/src/datascience-ui/history-react/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/history-react/images/Cancel/Cancel_16xMD_vscode.svg b/src/datascience-ui/history-react/images/Cancel/Cancel_16xMD_vscode.svg deleted file mode 100644 index 34f9c99b8509..000000000000 --- a/src/datascience-ui/history-react/images/Cancel/Cancel_16xMD_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>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> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/Cancel/Cancel_16xMD_vscode_dark.svg b/src/datascience-ui/history-react/images/Cancel/Cancel_16xMD_vscode_dark.svg deleted file mode 100644 index dc96b7ea9c1a..000000000000 --- a/src/datascience-ui/history-react/images/Cancel/Cancel_16xMD_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>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> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/CollapseAll/CollapseAll_16x_vscode.svg b/src/datascience-ui/history-react/images/CollapseAll/CollapseAll_16x_vscode.svg deleted file mode 100644 index 165211e95dbe..000000000000 --- a/src/datascience-ui/history-react/images/CollapseAll/CollapseAll_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;}.icon-vs-action-blue{fill:#00539c;}</style></defs><title>CollapseAll_16x</title><g id="canvas"><path id="_Compound_Path_" data-name="&lt;Compound Path&gt;" class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g id="outline" style="display: none;"><path id="_Compound_Path_2" data-name="&lt;Compound Path&gt;" class="icon-vs-out" d="M15,10H13v2H11v2H2V5H4V3H6V1h9Z" style="display: none;"/></g><g id="iconBg"><path id="_Compound_Path_3" data-name="&lt;Compound Path&gt;" class="icon-vs-bg" d="M14,2V9H13V3H7V2ZM5,4V5h6v6h1V4Zm5,2v7H3V6ZM9,7H4v5H9Z"/><path class="icon-vs-action-blue" d="M8,9v1H5V9Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/CollapseAll/CollapseAll_16x_vscode_dark.svg b/src/datascience-ui/history-react/images/CollapseAll/CollapseAll_16x_vscode_dark.svg deleted file mode 100644 index a36a8bea09c5..000000000000 --- a/src/datascience-ui/history-react/images/CollapseAll/CollapseAll_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;}.icon-vs-action-blue{fill:#75beff;}</style></defs><title>CollapseAll_16x</title><g id="canvas"><path id="_Compound_Path_" data-name="&lt;Compound Path&gt;" class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g id="outline" style="display: none;"><path id="_Compound_Path_2" data-name="&lt;Compound Path&gt;" class="icon-vs-out" d="M15,10H13v2H11v2H2V5H4V3H6V1h9Z" style="display: none;"/></g><g id="iconBg"><path id="_Compound_Path_3" data-name="&lt;Compound Path&gt;" class="icon-vs-bg" d="M14,2V9H13V3H7V2ZM5,4V5h6v6h1V4Zm5,2v7H3V6ZM9,7H4v5H9Z"/><path class="icon-vs-action-blue" d="M8,9v1H5V9Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/ExpandAll/ExpandAll_16x_vscode.svg b/src/datascience-ui/history-react/images/ExpandAll/ExpandAll_16x_vscode.svg deleted file mode 100644 index b20f6b55358a..000000000000 --- a/src/datascience-ui/history-react/images/ExpandAll/ExpandAll_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;}.icon-vs-action-blue{fill:#00539c;}</style></defs><title>ExpandAll_16x</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="M15,1v9H13v2H11v2H2V5H4V3H6V1Z" style="display: none;"/></g><g id="iconBg"><path id="_Compound_Path_" data-name="&lt;Compound Path&gt;" class="icon-vs-bg" d="M14,2V9H13V3H7V2ZM5,4V5h6v6h1V4Zm5,2v7H3V6ZM9,7H4v5H9Z"/><path id="_Compound_Path_2" data-name="&lt;Compound Path&gt;" class="icon-vs-action-blue" d="M7,9H8v1H7l-.01,1H6V10H5V9H6V8H7Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/ExpandAll/ExpandAll_16x_vscode_dark.svg b/src/datascience-ui/history-react/images/ExpandAll/ExpandAll_16x_vscode_dark.svg deleted file mode 100644 index 8bb67292fcb9..000000000000 --- a/src/datascience-ui/history-react/images/ExpandAll/ExpandAll_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;}.icon-vs-action-blue{fill:#75beff;}</style></defs><title>ExpandAll_16x</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="M15,1v9H13v2H11v2H2V5H4V3H6V1Z" style="display: none;"/></g><g id="iconBg"><path id="_Compound_Path_" data-name="&lt;Compound Path&gt;" class="icon-vs-bg" d="M14,2V9H13V3H7V2ZM5,4V5h6v6h1V4Zm5,2v7H3V6ZM9,7H4v5H9Z"/><path id="_Compound_Path_2" data-name="&lt;Compound Path&gt;" class="icon-vs-action-blue" d="M7,9H8v1H7l-.01,1H6V10H5V9H6V8H7Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/GoToSourceCode/GoToSourceCode_16x_vscode.svg b/src/datascience-ui/history-react/images/GoToSourceCode/GoToSourceCode_16x_vscode.svg deleted file mode 100644 index 4c9696edd9ae..000000000000 --- a/src/datascience-ui/history-react/images/GoToSourceCode/GoToSourceCode_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;}.icon-vs-action-blue{fill:#00539c;}</style></defs><title>GoToSourceCode_16x</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,3v.879L6.354.232,4.232,2.354,5.879,4H0V7H5.879L4.232,8.646,4.586,9H2v3H6v3h9V12H14V9h1V6h1V3ZM9,9H8.121L9,8.121Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-bg" d="M13,11H3V10H13ZM7,14h7V13H7ZM11,4V5h4V4ZM10,8h4V7H10Z"/></g><g id="colorAction"><path class="icon-vs-action-blue" d="M10.207,5.5,6.354,9.354l-.708-.708L8.293,6H1V5H8.293L5.646,2.354l.708-.708Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/GoToSourceCode/GoToSourceCode_16x_vscode_dark.svg b/src/datascience-ui/history-react/images/GoToSourceCode/GoToSourceCode_16x_vscode_dark.svg deleted file mode 100644 index 8bdfd6f52a33..000000000000 --- a/src/datascience-ui/history-react/images/GoToSourceCode/GoToSourceCode_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;}.icon-vs-action-blue{fill:#75beff;}</style></defs><title>GoToSourceCode_16x</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,3v.879L6.354.232,4.232,2.354,5.879,4H0V7H5.879L4.232,8.646,4.586,9H2v3H6v3h9V12H14V9h1V6h1V3ZM9,9H8.121L9,8.121Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-bg" d="M13,11H3V10H13ZM7,14h7V13H7ZM11,4V5h4V4ZM10,8h4V7H10Z"/></g><g id="colorAction"><path class="icon-vs-action-blue" d="M10.207,5.5,6.354,9.354l-.708-.708L8.293,6H1V5H8.293L5.646,2.354l.708-.708Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/Interrupt/Interrupt_16x_vscode.svg b/src/datascience-ui/history-react/images/Interrupt/Interrupt_16x_vscode.svg deleted file mode 100644 index 1e658beef8f0..000000000000 --- a/src/datascience-ui/history-react/images/Interrupt/Interrupt_16x_vscode.svg +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.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"> - .icon_x002D_canvas_x002D_transparent{opacity:0;fill:#F6F6F6;} - .icon_x002D_vs_x002D_out{fill:#F6F6F6;} - .icon_x002D_vs_x002D_bg{fill:#424242;} -</style> -<g id="canvas"> - <path class="icon_x002D_canvas_x002D_transparent" d="M16,16H0V0h16V16z"/> -</g> -<g id="outline" style="display: none;"> - <path class="icon_x002D_vs_x002D_out" d="M13,13H3V3h10V13z"/> -</g> -<g id="iconBg"> - <path class="icon_x002D_vs_x002D_bg" d="M12,12H4V4h8V12z"/> - <g> - </g> - <g> - </g> - <g> - </g> - <g> - </g> - <g> - </g> - <g> - </g> -</g> -</svg> diff --git a/src/datascience-ui/history-react/images/Interrupt/Interrupt_16x_vscode_dark.svg b/src/datascience-ui/history-react/images/Interrupt/Interrupt_16x_vscode_dark.svg deleted file mode 100644 index aae52c930ddb..000000000000 --- a/src/datascience-ui/history-react/images/Interrupt/Interrupt_16x_vscode_dark.svg +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.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"> - .icon_x002D_canvas_x002D_transparent{opacity:0;fill:#F6F6F6;} - .icon_x002D_vs_x002D_out{fill:#F6F6F6;} - .icon_x002D_vs_x002D_bg{fill:#c5c5c5;} -</style> -<g id="canvas"> - <path class="icon_x002D_canvas_x002D_transparent" d="M16,16H0V0h16V16z"/> -</g> -<g id="outline" style="display: none;"> - <path class="icon_x002D_vs_x002D_out" d="M13,13H3V3h10V13z"/> -</g> -<g id="iconBg"> - <path class="icon_x002D_vs_x002D_bg" d="M12,12H4V4h8V12z"/> - <g> - </g> - <g> - </g> - <g> - </g> - <g> - </g> - <g> - </g> - <g> - </g> -</g> -</svg> diff --git a/src/datascience-ui/history-react/images/PopIn/PopIn_16x_vscode.svg b/src/datascience-ui/history-react/images/PopIn/PopIn_16x_vscode.svg deleted file mode 100644 index f6628fbd98ed..000000000000 --- a/src/datascience-ui/history-react/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 id="canvas"><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M16,0V15H13V5.121L7.121,11H12v3H2V4H5V8.879L10.879,3H1V0Z" style="display: none;"/></g><g id="iconBg"><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/history-react/images/PopIn/PopIn_16x_vscode_dark.svg b/src/datascience-ui/history-react/images/PopIn/PopIn_16x_vscode_dark.svg deleted file mode 100644 index 0acae5b9e9de..000000000000 --- a/src/datascience-ui/history-react/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 id="canvas"><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M16,0V15H13V5.121L7.121,11H12v3H2V4H5V8.879L10.879,3H1V0Z" style="display: none;"/></g><g id="iconBg"><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/history-react/images/PopOut/PopOut_16x_vscode.svg b/src/datascience-ui/history-react/images/PopOut/PopOut_16x_vscode.svg deleted file mode 100644 index 3acc3152a115..000000000000 --- a/src/datascience-ui/history-react/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 id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" 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 id="iconBg"><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/history-react/images/PopOut/PopOut_16x_vscode_dark.svg b/src/datascience-ui/history-react/images/PopOut/PopOut_16x_vscode_dark.svg deleted file mode 100644 index 90903838b070..000000000000 --- a/src/datascience-ui/history-react/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 id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" 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 id="iconBg"><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/history-react/images/Redo/Redo_16x_vscode.svg b/src/datascience-ui/history-react/images/Redo/Redo_16x_vscode.svg deleted file mode 100644 index 34e0464c5fdf..000000000000 --- a/src/datascience-ui/history-react/images/Redo/Redo_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-action-blue{fill:#00539c;}</style></defs><title>Redo_16x</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="M2.9,1.736A5.935,5.935,0,0,1,11,1.474V0h4V8H7V4.011a2.036,2.036,0,0,0-1.332.61,1.93,1.93,0,0,0,0,2.727l5.945,5.945L8.906,16H8.664L2.84,10.176a5.857,5.857,0,0,1-1.728-4.2A6.009,6.009,0,0,1,2.9,1.736Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-action-blue" d="M3.6,2.443a4.933,4.933,0,0,1,6.969,0L12,3.872V1h2V7H8V5h2.3L9.158,3.857a2.949,2.949,0,0,0-4.2.057,2.93,2.93,0,0,0,0,4.141L10.2,13.293,8.785,14.707,3.547,9.469A4.951,4.951,0,0,1,3.6,2.443Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/Redo/Redo_16x_vscode_dark.svg b/src/datascience-ui/history-react/images/Redo/Redo_16x_vscode_dark.svg deleted file mode 100644 index 5a245967edec..000000000000 --- a/src/datascience-ui/history-react/images/Redo/Redo_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-action-blue{fill:#75beff;}</style></defs><title>Redo_16x</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="M2.9,1.736A5.935,5.935,0,0,1,11,1.474V0h4V8H7V4.011a2.036,2.036,0,0,0-1.332.61,1.93,1.93,0,0,0,0,2.727l5.945,5.945L8.906,16H8.664L2.84,10.176a5.857,5.857,0,0,1-1.728-4.2A6.009,6.009,0,0,1,2.9,1.736Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-action-blue" d="M3.6,2.443a4.933,4.933,0,0,1,6.969,0L12,3.872V1h2V7H8V5h2.3L9.158,3.857a2.949,2.949,0,0,0-4.2.057,2.93,2.93,0,0,0,0,4.141L10.2,13.293,8.785,14.707,3.547,9.469A4.951,4.951,0,0,1,3.6,2.443Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/Restart/Restart_grey_16x_vscode.svg b/src/datascience-ui/history-react/images/Restart/Restart_grey_16x_vscode.svg deleted file mode 100644 index 4918f0cf24d4..000000000000 --- a/src/datascience-ui/history-react/images/Restart/Restart_grey_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>Restart_grey_16x</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="M8,0A7.989,7.989,0,0,0,4,1.088V0H0V8.673l.11.657A8,8,0,1,0,8,0ZM8,12A3.982,3.982,0,0,1,4.056,8.669L3.943,8H8V4a4,4,0,0,1,0,8Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-bg" d="M15,8A7,7,0,0,1,1.1,9.165l1.972-.331A5,5,0,1,0,4,5H7V7H1V1H3V3.12A6.987,6.987,0,0,1,15,8Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/Restart/Restart_grey_16x_vscode_dark.svg b/src/datascience-ui/history-react/images/Restart/Restart_grey_16x_vscode_dark.svg deleted file mode 100644 index 4d159147069a..000000000000 --- a/src/datascience-ui/history-react/images/Restart/Restart_grey_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>Restart_grey_16x</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="M8,0A7.989,7.989,0,0,0,4,1.088V0H0V8.673l.11.657A8,8,0,1,0,8,0ZM8,12A3.982,3.982,0,0,1,4.056,8.669L3.943,8H8V4a4,4,0,0,1,0,8Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-bg" d="M15,8A7,7,0,0,1,1.1,9.165l1.972-.331A5,5,0,1,0,4,5H7V7H1V1H3V3.12A6.987,6.987,0,0,1,15,8Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/SaveAs/SaveAs_16x_vscode.svg b/src/datascience-ui/history-react/images/SaveAs/SaveAs_16x_vscode.svg deleted file mode 100644 index 4eb7f58b872f..000000000000 --- a/src/datascience-ui/history-react/images/SaveAs/SaveAs_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-action-blue{fill:#00539c;}.icon-vs-bg{fill:#424242;}</style></defs><title>SaveAs_16x</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="M15.906,7.544a2.543,2.543,0,0,1-.75,1.812l-5.795,5.8L5.973,16H4.623l.5-2H2.086L0,11.914V0H14V5.109a2.455,2.455,0,0,1,1.157.626A2.537,2.537,0,0,1,15.906,7.544Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-action-blue" d="M1,1V11.5L2.5,13H4V9H8.27l3.265-3.265A2.511,2.511,0,0,1,13,5.053V1ZM11,5H3V2h8ZM5,11H6.27l-.531.531L5.372,13H5Z"/><path class="icon-vs-bg" d="M5.907,14.985l.735-2.943,5.6-5.6a1.655,1.655,0,0,1,2.208,0,1.562,1.562,0,0,1,0,2.207l-5.6,5.6Zm1.638-2.431L7.281,13.61l1.057-.263,5.4-5.4a.561.561,0,0,0,0-.793.629.629,0,0,0-.793,0Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/SaveAs/SaveAs_16x_vscode_dark.svg b/src/datascience-ui/history-react/images/SaveAs/SaveAs_16x_vscode_dark.svg deleted file mode 100644 index 2a711add7ff8..000000000000 --- a/src/datascience-ui/history-react/images/SaveAs/SaveAs_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-action-blue{fill:#75beff;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>SaveAs_16x</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="M15.906,7.544a2.543,2.543,0,0,1-.75,1.812l-5.795,5.8L5.973,16H4.623l.5-2H2.086L0,11.914V0H14V5.109a2.455,2.455,0,0,1,1.157.626A2.537,2.537,0,0,1,15.906,7.544Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-action-blue" d="M1,1V11.5L2.5,13H4V9H8.27l3.265-3.265A2.511,2.511,0,0,1,13,5.053V1ZM11,5H3V2h8ZM5,11H6.27l-.531.531L5.372,13H5Z"/><path class="icon-vs-bg" d="M5.907,14.985l.735-2.943,5.6-5.6a1.655,1.655,0,0,1,2.208,0,1.562,1.562,0,0,1,0,2.207l-5.6,5.6Zm1.638-2.431L7.281,13.61l1.057-.263,5.4-5.4a.561.561,0,0,0,0-.793.629.629,0,0,0-.793,0Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/Undo/Undo_16x_vscode.svg b/src/datascience-ui/history-react/images/Undo/Undo_16x_vscode.svg deleted file mode 100644 index 85aa3a7ad80b..000000000000 --- a/src/datascience-ui/history-react/images/Undo/Undo_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-action-blue{fill:#00539c;}</style></defs><title>Undo_16x</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="M14.888,5.972a5.853,5.853,0,0,1-1.728,4.2L7.336,16H7.094L4.387,13.293l5.945-5.945a1.928,1.928,0,0,0,0-2.727A2.036,2.036,0,0,0,9,4.011V8H1V0H5V1.474a5.934,5.934,0,0,1,8.1.262A6.006,6.006,0,0,1,14.888,5.972Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-action-blue" d="M12.453,9.469,7.215,14.707,5.8,13.293l5.238-5.238a2.927,2.927,0,0,0,0-4.141,2.949,2.949,0,0,0-4.2-.057L5.7,5H8V7H2V1H4V3.872L5.428,2.443a4.968,4.968,0,0,1,7.025,7.026Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/history-react/images/Undo/Undo_16x_vscode_dark.svg b/src/datascience-ui/history-react/images/Undo/Undo_16x_vscode_dark.svg deleted file mode 100644 index 7530f28a2f27..000000000000 --- a/src/datascience-ui/history-react/images/Undo/Undo_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-action-blue{fill:#75beff;}</style></defs><title>Undo_16x</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="M14.888,5.972a5.853,5.853,0,0,1-1.728,4.2L7.336,16H7.094L4.387,13.293l5.945-5.945a1.928,1.928,0,0,0,0-2.727A2.036,2.036,0,0,0,9,4.011V8H1V0H5V1.474a5.934,5.934,0,0,1,8.1.262A6.006,6.006,0,0,1,14.888,5.972Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-action-blue" d="M12.453,9.469,7.215,14.707,5.8,13.293l5.238-5.238a2.927,2.927,0,0,0,0-4.141,2.949,2.949,0,0,0-4.2-.057L5.7,5H8V7H2V1H4V3.872L5.428,2.443a4.968,4.968,0,0,1,7.025,7.026Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/history-react/index.css b/src/datascience-ui/history-react/index.css deleted file mode 100644 index b4cc7250b98c..000000000000 --- a/src/datascience-ui/history-react/index.css +++ /dev/null @@ -1,5 +0,0 @@ -body { - margin: 0; - padding: 0; - font-family: sans-serif; -} diff --git a/src/datascience-ui/history-react/index.html b/src/datascience-ui/history-react/index.html deleted file mode 100644 index a54083311825..000000000000 --- a/src/datascience-ui/history-react/index.html +++ /dev/null @@ -1,348 +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> - <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; } - - 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; - } - </script> - </body> -</html> diff --git a/src/datascience-ui/history-react/index.tsx b/src/datascience-ui/history-react/index.tsx deleted file mode 100644 index 785717d091d2..000000000000 --- a/src/datascience-ui/history-react/index.tsx +++ /dev/null @@ -1,18 +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 { PostOffice } from '../react-common/postOffice'; -import { detectTheme } from '../react-common/themeDetector'; -import './index.css'; -import { MainPanel } from './MainPanel'; - -const theme = detectTheme(); -const skipDefault = PostOffice.canSendMessages(); - -ReactDOM.render( - <MainPanel theme={theme} skipDefault={skipDefault} />, - document.getElementById('root') as HTMLElement -); diff --git a/src/datascience-ui/history-react/mainPanel.css b/src/datascience-ui/history-react/mainPanel.css deleted file mode 100644 index 2d0d501d68a8..000000000000 --- a/src/datascience-ui/history-react/mainPanel.css +++ /dev/null @@ -1,3 +0,0 @@ -.top-spacing { - margin-top : 24px; -} diff --git a/src/datascience-ui/history-react/mainPanelState.ts b/src/datascience-ui/history-react/mainPanelState.ts deleted file mode 100644 index 12622148ce83..000000000000 --- a/src/datascience-ui/history-react/mainPanelState.ts +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { nbformat } from '@jupyterlab/coreutils'; - -import * as path from 'path'; -import { concatMultilineString } from '../../client/datascience/common'; -import { CellState, ICell, ISysInfo } from '../../client/datascience/types'; -import { ICellViewModel } from './cell'; - -export interface IMainPanelState { - cellVMs: ICellViewModel[]; - busy: boolean; - skipNextScroll? : boolean; - undoStack : ICellViewModel[][]; - redoStack : ICellViewModel[][]; -} - -// This function generates test state when running under a browser instead of inside of -export function generateTestState(inputBlockToggled : (id: string) => void, filePath: string = '') : IMainPanelState { - return { - cellVMs : generateVMs(inputBlockToggled, filePath), - busy: true, - skipNextScroll : false, - undoStack : [], - redoStack : [] - }; -} - -export function createCellVM(inputCell: ICell, inputBlockToggled : (id: string) => void) : ICellViewModel { - let inputLinesCount = 0; - let source = inputCell.data.cell_type === 'code' ? inputCell.data.source : []; - - // Eliminate the #%% on the front if it has nothing else on the line - if (source.length > 0 && /^\s*#\s*%%\s*$/.test(source[0].trim())) { - source = source.slice(1); - } - - const inputText = inputCell.data.cell_type === 'code' ? concatMultilineString(source) : ''; - if (inputText) { - inputLinesCount = inputText.split('\n').length; - } - - return { - cell: inputCell, - inputBlockOpen: true, - inputBlockText: inputText, - inputBlockCollapseNeeded: inputLinesCount > 1, - inputBlockToggled: inputBlockToggled - }; -} - -function generateVMs(inputBlockToggled : (id: string) => void, filePath: string) : ICellViewModel [] { - const cells = generateCells(filePath); - return cells.map((cell : ICell) => { - return createCellVM(cell, inputBlockToggled); - }); -} - -function generateCells(filePath: string) : ICell[] { - const cellData = generateCellData(); - return cellData.map((data : nbformat.ICodeCell | nbformat.IMarkdownCell | nbformat.IRawCell | ISysInfo, key : number) => { - return { - id : key.toString(), - file : path.join(filePath, 'foo.py'), - line : 1, - state: key === cellData.length - 1 ? CellState.executing : CellState.finished, - data : data - }; - }); -} - -//tslint:disable:max-func-body-length -function generateCellData() : (nbformat.ICodeCell | nbformat.IMarkdownCell | nbformat.IRawCell | ISysInfo)[] { - - // Hopefully new entries here can just be copied out of a jupyter notebook (ipynb) - return [ - { - // These are special. Sys_info is our own custom cell - cell_type: 'sys_info', - path: 'c:\\data\\python.exe', - version : '3.9.9.9 The Uber Version', - notebook_version: '(5, 9, 9)', - source: [], - metadata: {}, - message: 'You have this python data:', - connection: 'https:\\localhost' - }, - { - cell_type: 'code', - execution_count: 4, - metadata: { - slideshow: { - slide_type: '-' - } - }, - outputs: [ - { - data: { - 'text/plain': [ - ' num_preg glucose_conc diastolic_bp thickness insulin bmi diab_pred \\\n', - '0 6 148 72 35 0 33.6 0.627 \n', - '1 1 85 66 29 0 26.6 0.351 \n', - '2 8 183 64 0 0 23.3 0.672 \n', - '3 1 89 66 23 94 28.1 0.167 \n', - '4 0 137 40 35 168 43.1 2.288 \n', - '\n', - ' age skin diabetes \n', - '0 50 1.3790 True \n', - '1 31 1.1426 False \n', - '2 32 0.0000 True \n', - '3 21 0.9062 False \n', - '4 33 1.3790 True super long line that should wrap around but it isnt because we didnt put in the correct css super long line that should wrap around but it isnt because we didnt put in the correct css super long line that should wrap around but it isnt because we didnt put in the correct css' - ] - }, - execution_count: 4, - metadata: {}, - output_type: 'execute_result' - } - ], - source: [ - '# comment', - - 'df', - 'df.head(5)' - ] - }, - { - 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/history-react/menuBar.css b/src/datascience-ui/history-react/menuBar.css deleted file mode 100644 index b6d3746517e9..000000000000 --- a/src/datascience-ui/history-react/menuBar.css +++ /dev/null @@ -1,55 +0,0 @@ - -.menuBar{ - position: relative; - margin-bottom: 2px; - display:flex; - flex-direction: row-reverse; - justify-content: right; -} - -.menuBar:after{ - content: ""; - clear: both; - display: block; -} - -.menuBar-top{ - position: top; - display:flex; - flex-direction: row-reverse; - justify-content: right; -} - -.menuBar-top:after{ - content: ""; - clear: both; - display: block; -} - -.menuBar-top-fixed{ - z-index: 10; - position: fixed; - top: 0; - left: 0; - right: 0; - display:flex; - flex-direction: row-reverse; - justify-content: right; - padding-bottom: 2px; - padding-top: 2px; - padding-right: 5px; - border-bottom-width: 1px; - border-top-width: 0px; - border-right-width: 0px; - border-left-width: 0px; - margin-bottom: 1px; - border-style:solid; - background-color: var(--vscode-editor-background); - border-color: var(--vscode-editorGroupHeader-tabsBackground); -} - -.menuBar-top-fixed:after{ - content: ""; - clear: both; - display: block; -} diff --git a/src/datascience-ui/history-react/menuBar.tsx b/src/datascience-ui/history-react/menuBar.tsx deleted file mode 100644 index 0a1da585b0f4..000000000000 --- a/src/datascience-ui/history-react/menuBar.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import './menuBar.css'; - -import * as React from 'react'; - -interface IMenuBarProps { - theme: string; - stylePosition? : string; -} - -// Simple 'bar'. Came up with the css by playing around here: -// https://www.w3schools.com/cssref/tryit.asp?filename=trycss_float -export class MenuBar extends React.Component<IMenuBarProps> { - constructor(props) { - super(props); - } - - public render() { - const classNames = this.props.stylePosition ? - `menuBar-${this.props.stylePosition} menuBar-${this.props.stylePosition}-${this.props.theme}` - : 'menuBar'; - return ( - <div className={classNames}> - {this.props.children} - </div> - ); - } -} diff --git a/src/datascience-ui/history-react/sysInfo.css b/src/datascience-ui/history-react/sysInfo.css deleted file mode 100644 index f0a2e18a3ebd..000000000000 --- a/src/datascience-ui/history-react/sysInfo.css +++ /dev/null @@ -1,23 +0,0 @@ -.sysinfo-wrapper { - margin: 10px; - padding: 2px; - display: block; - border-bottom-color: var(--vscode-editorGroupHeader-tabsBackground); - border-bottom-style: solid; - border-bottom-width: 1px; -} - -.sysinfo-outer { - background: var(--vscode-notifications-background); - white-space: pre-wrap; - font-family: monospace; - width: 100%; -} - -.sysinfo-result-container pre { - white-space: pre-wrap; - font-family: monospace; -} - - - diff --git a/src/datascience-ui/history-react/sysInfo.tsx b/src/datascience-ui/history-react/sysInfo.tsx deleted file mode 100644 index 148cd0ce6149..000000000000 --- a/src/datascience-ui/history-react/sysInfo.tsx +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './sysInfo.css'; - -import * as React from 'react'; - -// tslint:disable-next-line:match-default-export-name import-name -interface ISysInfoProps -{ - message: string; - path: string; - notebook_version: string; - version: string; - theme: string; - connection: string; -} - -export class SysInfo extends React.Component<ISysInfoProps> { - constructor(prop: ISysInfoProps) { - super(prop); - } - - public render() { - const connectionString = this.props.connection.length > 0 ? `${this.props.connection}\r\n` : ''; - const output = `${connectionString}${this.props.message}\r\n${this.props.version}\r\n${this.props.path}\r\n${this.props.notebook_version}`; - - return ( - <div className='sysinfo-wrapper'> - <div className='sysinfo-outer'> - <div className='sysinfo-result-container'> - <pre><span>{output}</span></pre> - </div> - </div> - </div> - ); - } -} diff --git a/src/datascience-ui/history-react/transforms.ts b/src/datascience-ui/history-react/transforms.ts deleted file mode 100644 index 0ca03c11cf27..000000000000 --- a/src/datascience-ui/history-react/transforms.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* tslint:disable */ -'use strict'; - -// This code is from @nteract/transforms-full except without the Vega transforms: -// https://github.com/nteract/nteract/blob/v0.12.2/packages/transforms-full/src/index.js . -// Vega transforms mess up our npm pkg install because they rely on the npm canvas module that needs -// to be built on each system. - -import PlotlyTransform, { - PlotlyNullTransform -} from "@nteract/transform-plotly"; -import GeoJSONTransform from "@nteract/transform-geojson"; - -import ModelDebug from "@nteract/transform-model-debug"; - -import DataResourceTransform from "@nteract/transform-dataresource"; - -// import { VegaLite1, VegaLite2, Vega2, Vega3 } from "@nteract/transform-vega"; - -import { - standardTransforms, - standardDisplayOrder, - registerTransform, - richestMimetype -} from "@nteract/transforms"; - -const additionalTransforms = [ - DataResourceTransform, - ModelDebug, - PlotlyNullTransform, - PlotlyTransform, - GeoJSONTransform, -]; - -const { transforms, displayOrder } = additionalTransforms.reduce( - registerTransform, - { - transforms: standardTransforms, - displayOrder: standardDisplayOrder - } -); - -export { displayOrder, transforms, richestMimetype, registerTransform }; diff --git a/src/datascience-ui/react-common/errorBoundary.tsx b/src/datascience-ui/react-common/errorBoundary.tsx deleted file mode 100644 index 586f26fdf340..000000000000 --- a/src/datascience-ui/react-common/errorBoundary.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'; - -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, info) { - const stack = info.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 = {}; - // 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/locReactSide.ts b/src/datascience-ui/react-common/locReactSide.ts deleted file mode 100644 index 5ccf1c716dce..000000000000 --- a/src/datascience-ui/react-common/locReactSide.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// The WebPanel constructed by the extension should inject a getLocStrings function into -// the script. This should return a dictionary of key value pairs for loc strings -export declare function getLocStrings() : { [index: string ] : string }; - -// The react code can't use the localize.ts module because it reads from -// disk. This isn't allowed inside a browswer, so we pass the collection -// through the javascript. -let loadedCollection: { [index: string]: string } | undefined ; - -export function getLocString(key: string, defValue: string) : string { - if (!loadedCollection) { - load(); - } - - if (loadedCollection && loadedCollection.hasOwnProperty(key)) { - return loadedCollection[key]; - } - - return defValue; -} - -function load() { - // tslint:disable-next-line:no-typeof-undefined - if (typeof getLocStrings !== 'undefined') { - loadedCollection = getLocStrings(); - } else { - loadedCollection = {}; - } -} diff --git a/src/datascience-ui/react-common/postOffice.tsx b/src/datascience-ui/react-common/postOffice.tsx deleted file mode 100644 index 883709ee6e60..000000000000 --- a/src/datascience-ui/react-common/postOffice.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 { WebPanelMessage } from '../../client/common/application/types'; - -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; -} - -interface IPostOfficeProps { - messageHandlers: IMessageHandler[]; -} - -// This special function talks to vscode from a web panel -export declare function acquireVsCodeApi(): IVsCodeApi; - -export class PostOffice extends React.Component<IPostOfficeProps> { - - private static vscodeApi : IVsCodeApi | undefined; - private registered: boolean = false; - - constructor(props: IPostOfficeProps) { - super(props); - } - - public static canSendMessages() { - if (PostOffice.acquireApi()) { - return true; - } - return false; - } - - public static sendMessage(message: WebPanelMessage) { - if (PostOffice.canSendMessages()) { - const api = PostOffice.acquireApi(); - if (api) { - api.postMessage(message); - } - } - } - - private static acquireApi() : IVsCodeApi | undefined { - - // Only do this once as it crashes if we ask more than once - if (!PostOffice.vscodeApi && - // tslint:disable-next-line:no-typeof-undefined - typeof acquireVsCodeApi !== 'undefined') { - PostOffice.vscodeApi = acquireVsCodeApi(); - } - - return PostOffice.vscodeApi; - } - - public componentDidMount() { - if (!this.registered) { - this.registered = true; - window.addEventListener('message', this.handleMessages); - } - } - - public componentWillUnmount() { - if (this.registered) { - this.registered = false; - window.removeEventListener('message', this.handleMessages); - } - } - - public render() { - return null; - } - - private handleMessages = async (ev: MessageEvent) => { - if (this.props) { - const msg = ev.data as WebPanelMessage; - if (msg) { - this.props.messageHandlers.forEach((h : IMessageHandler) => { - 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 da758f1bf386..000000000000 --- a/src/datascience-ui/react-common/progress.tsx +++ /dev/null @@ -1,20 +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/relativeImage.tsx b/src/datascience-ui/react-common/relativeImage.tsx deleted file mode 100644 index 907e7fb9b8ba..000000000000 --- a/src/datascience-ui/react-common/relativeImage.tsx +++ /dev/null @@ -1,37 +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/themeDetector.ts b/src/datascience-ui/react-common/themeDetector.ts deleted file mode 100644 index bc11d1451618..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 detectTheme() : '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/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/launch.json b/src/test/.vscode/launch.json new file mode 100644 index 000000000000..a139754d2c07 --- /dev/null +++ b/src/test/.vscode/launch.json @@ -0,0 +1,37 @@ +{ + "version": "0.1.0", + "configurations": [ + { + "name": "launch a file", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + }, + { + "name": "attach to a local port", + "type": "python", + "request": "attach", + "port": 5678, + "host": "localhost", + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "." + } + ] + }, + { + "name": "attach to a local PID", + "type": "python", + "request": "attach", + "processId": "${env:CI_DEBUGPY_PROCESS_ID}", + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "." + } + ] + } + ] +} diff --git a/src/test/.vscode/launch.json.README b/src/test/.vscode/launch.json.README new file mode 100644 index 000000000000..644e7e47253a --- /dev/null +++ b/src/test/.vscode/launch.json.README @@ -0,0 +1,3 @@ +// These configs are used in full-stack integration tests. +// They mostly borrow from the code in src/client/debugger/extension/configuration/providers. + diff --git a/src/test/.vscode/settings.json b/src/test/.vscode/settings.json index 34897b48e013..cd2b4152591d 100644 --- a/src/test/.vscode/settings.json +++ b/src/test/.vscode/settings.json @@ -1,26 +1,18 @@ { "python.linting.pylintEnabled": false, "python.linting.flake8Enabled": false, - "python.workspaceSymbols.enabled": false, - "python.unitTest.nosetestArgs": [], - "python.unitTest.pyTestArgs": [], - "python.unitTest.unittestArgs": [ - "-s=./tests", - "-p=test_*.py" - ], - "python.sortImports.args": [], + "python.testing.pytestArgs": [], + "python.testing.unittestArgs": ["-s=./tests", "-p=test_*.py", "-v", "-s", ".", "-p", "*test*.py"], "python.linting.lintOnSave": false, "python.linting.enabled": true, - "python.linting.pep8Enabled": false, + "python.linting.pycodestyleEnabled": false, "python.linting.prospectorEnabled": false, "python.linting.pydocstyleEnabled": false, "python.linting.pylamaEnabled": false, "python.linting.mypyEnabled": false, "python.linting.banditEnabled": false, - "python.formatting.provider": "yapf", - "python.linting.pylintUseMinimalCheckers": false - // Do not set this to true/false even when LS is the default, else - // it will result in LS being downloaded on CI and slow down tests significantly. - // We have other tests on CI for testing downloading of CI with this setting enabled. - // "python.jediEnabled": true + // 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 c4371e74af04..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\\pep8config\\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\\unitestsWithConfigs\\other\\test_pytest.py /^class Test_CheckMyApp:$/;" kind:class line:6 -Test_CheckMyApp ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_pytest.py /^ class Test_NestedClassA:$/;" kind:class line:13 -Test_NestedClassA ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_pytest.py /^ class Test_nested_classB_Of_A:$/;" kind:class line:16 -Test_nested_classB_Of_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_unittest_one.py /^class Test_test1(unittest.TestCase):$/;" kind:class line:6 -Test_test1 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\pep8config\\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\\pep8config\\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\\pep8config\\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\\pep8config\\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\\pep8config\\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\\pep8config\\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\\pep8config\\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\\pep8config\\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\\pep8config\\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\\pep8config\\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\\pep8config\\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\\unitestsWithConfigs\\other\\test_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:33 -non_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_another_pytest.py /^def non_parametrized_username(request):$/;" kind:function line:10 -non_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_pytest.py /^def parametrized_username():$/;" kind:function line:29 -parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_another_pytest.py /^def parametrized_username():$/;" kind:function line:6 -parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_unittest_one.py /^ def test_A(self):$/;" kind:member line:7 -test_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_one.py /^ def test_A(self):$/;" kind:member line:7 -test_A ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_unittest_one.py /^ def test_B(self):$/;" kind:member line:10 -test_B ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_unittest_one.py /^ def test_B(self):$/;" kind:member line:10 -test_B ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_unittest_one.py /^ def test_c(self):$/;" kind:member line:14 -test_c ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_complex_check(self):$/;" kind:member line:10 -test_complex_check ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_complex_check2(self):$/;" kind:member line:24 -test_complex_check2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_d(self):$/;" kind:member line:17 -test_d ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_nested_class_methodB(self):$/;" kind:member line:14 -test_nested_class_methodB ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_nested_class_methodC(self):$/;" kind:member line:19 -test_nested_class_methodC ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:39 -test_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_another_pytest.py /^def test_parametrized_username(non_parametrized_username):$/;" kind:function line:16 -test_parametrized_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_pytest.py 1;" kind:file line:1 -test_pytest.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_simple_check(self):$/;" kind:member line:8 -test_simple_check ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_pytest.py /^ def test_simple_check2(self):$/;" kind:member line:22 -test_simple_check2 ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_unittest_one.py 1;" kind:file line:1 -test_unittest_one.py ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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\\unitestsWithConfigs\\other\\test_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:36 -test_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\tests\\test_another_pytest.py /^def test_username(parametrized_username):$/;" kind:function line:13 -test_username ..\\pythonFiles\\testFiles\\unitestsWithConfigs\\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\\unitestsWithConfigs\\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/aaFirstTest/aaFirstTest.test.ts b/src/test/aaFirstTest/aaFirstTest.test.ts deleted file mode 100644 index 0e2d101ff9e0..000000000000 --- a/src/test/aaFirstTest/aaFirstTest.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import { extensions } from 'vscode'; -import { PVSC_EXTENSION_ID } from '../../client/common/constants'; -import { initialize } from '../initialize'; - -// NOTE: -// We need this to be run first, as this ensures the extension activates. -// Sometimes it can take more than 25 seconds to complete (as the extension looks for interpeters, and the like). -// So lets wait for a max of 1 minute for the extension to activate (note, subsequent load times are faster). - -suite('Activate Extension', () => { - suiteSetup(async function () { - // tslint:disable-next-line:no-invalid-this - this.timeout(60000); - await initialize(); - }); - test('Python extension has activated', async () => { - expect(extensions.getExtension(PVSC_EXTENSION_ID)!.isActive).to.equal(true, 'Extension has not been activated'); - }); -}); diff --git a/src/test/activation/activationManager.unit.test.ts b/src/test/activation/activationManager.unit.test.ts new file mode 100644 index 000000000000..6ee2572214b8 --- /dev/null +++ b/src/test/activation/activationManager.unit.test.ts @@ -0,0 +1,419 @@ +// 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 { TextDocument, Uri, WorkspaceFolder } from 'vscode'; +import { ExtensionActivationManager } from '../../client/activation/activationManager'; +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 { FileSystem } from '../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../client/common/platform/types'; +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', () => { + suite('Language Server Activation - ActivationManager', () => { + class ExtensionActivationManagerTest extends ExtensionActivationManager { + public addHandlers() { + return super.addHandlers(); + } + + public async initialize() { + return super.initialize(); + } + + public addRemoveDocOpenedHandlers() { + super.addRemoveDocOpenedHandlers(); + } + } + let managerTest: ExtensionActivationManagerTest; + let workspaceService: IWorkspaceService; + let appDiagnostics: typemoq.IMock<IApplicationDiagnostics>; + let autoSelection: typemoq.IMock<IInterpreterAutoSelectionService>; + let activeResourceService: IActiveResourceService; + let documentManager: typemoq.IMock<IDocumentManager>; + let interpreterPathService: typemoq.IMock<IInterpreterPathService>; + let fileSystem: IFileSystem; + setup(() => { + 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>(); + documentManager = typemoq.Mock.ofType<IDocumentManager>(); + 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( + [], + [], + documentManager.object, + autoSelection.object, + appDiagnostics.object, + instance(workspaceService), + instance(fileSystem), + instance(activeResourceService), + interpreterPathService.object, + ); + + sinon.stub(EnvFileTelemetry, 'sendActivationTelemetry').resolves(); + }); + + 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 unknown) as WorkspaceFolder, + (2 as unknown) as WorkspaceFolder, + ]); + const eventDef = () => disposable2.object; + documentManager + .setup((d) => d.onDidOpenTextDocument) + .returns(() => eventDef) + .verifiable(typemoq.Times.once()); + + await managerTest.initialize(); + + verify(workspaceService.workspaceFolders).once(); + verify(workspaceService.onDidChangeWorkspaceFolders).once(); + + documentManager.verifyAll(); + + disposable.setup((d) => d.dispose()).verifiable(typemoq.Times.once()); + disposable2.setup((d) => d.dispose()).verifiable(typemoq.Times.once()); + + managerTest.dispose(); + + disposable.verifyAll(); + disposable2.verifyAll(); + }); + test('Remove text document opened handler if there is only one workspace', async () => { + const disposable = typemoq.Mock.ofType<IDisposable>(); + const disposable2 = typemoq.Mock.ofType<IDisposable>(); + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(() => disposable.object); + when(workspaceService.workspaceFolders).thenReturn([ + (1 as unknown) as WorkspaceFolder, + (2 as unknown) as WorkspaceFolder, + ]); + const eventDef = () => disposable2.object; + documentManager + .setup((d) => d.onDidOpenTextDocument) + .returns(() => eventDef) + .verifiable(typemoq.Times.once()); + disposable.setup((d) => d.dispose()); + disposable2.setup((d) => d.dispose()); + + await managerTest.initialize(); + + verify(workspaceService.workspaceFolders).once(); + verify(workspaceService.onDidChangeWorkspaceFolders).once(); + documentManager.verifyAll(); + disposable.verify((d) => d.dispose(), typemoq.Times.never()); + disposable2.verify((d) => d.dispose(), typemoq.Times.never()); + + when(workspaceService.workspaceFolders).thenReturn([]); + + await managerTest.initialize(); + + disposable.verify((d) => d.dispose(), typemoq.Times.never()); + disposable2.verify((d) => d.dispose(), typemoq.Times.once()); + + managerTest.dispose(); + + disposable.verify((d) => d.dispose(), typemoq.Times.atLeast(1)); + disposable2.verify((d) => d.dispose(), typemoq.Times.once()); + }); + test('Activate workspace specific to the resource in case of Multiple workspaces when a file is opened', async () => { + const disposable1 = typemoq.Mock.ofType<IDisposable>(); + const disposable2 = typemoq.Mock.ofType<IDisposable>(); + let fileOpenedHandler!: (e: TextDocument) => Promise<void>; + // eslint-disable-next-line @typescript-eslint/ban-types + let workspaceFoldersChangedHandler!: Function; + const documentUri = Uri.file('a'); + const document = typemoq.Mock.ofType<TextDocument>(); + document.setup((d) => d.uri).returns(() => documentUri); + document.setup((d) => d.languageId).returns(() => PYTHON_LANGUAGE); + + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn((cb) => { + workspaceFoldersChangedHandler = cb; + return disposable1.object; + }); + documentManager + .setup((w) => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((cb) => { + fileOpenedHandler = cb; + }) + .returns(() => disposable2.object) + .verifiable(typemoq.Times.once()); + + const resource = Uri.parse('two'); + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const folder2 = { name: 'two', uri: resource, index: 2 }; + when(workspaceService.getWorkspaceFolderIdentifier(anything(), anything())).thenReturn('one'); + when(workspaceService.workspaceFolders).thenReturn([folder1, folder2]); + when(workspaceService.getWorkspaceFolder(document.object.uri)).thenReturn(folder2); + + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(folder2); + autoSelection + .setup((a) => a.autoSelectInterpreter(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + appDiagnostics + .setup((a) => a.performPreStartupHealthCheck(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + // Add workspaceFoldersChangedHandler + managerTest.addHandlers(); + expect(workspaceFoldersChangedHandler).not.to.be.equal(undefined, 'Handler not set'); + + // Add fileOpenedHandler + workspaceFoldersChangedHandler.call(managerTest); + expect(fileOpenedHandler).not.to.be.equal(undefined, 'Handler not set'); + + // Check if activate workspace is called on opening a file + await fileOpenedHandler.call(managerTest, document.object); + await sleep(1); + + documentManager.verifyAll(); + verify(workspaceService.onDidChangeWorkspaceFolders).once(); + verify(workspaceService.workspaceFolders).atLeast(1); + verify(workspaceService.getWorkspaceFolder(anything())).atLeast(1); + }); + + test("The same workspace isn't activated more than once", 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); + await managerTest.activateWorkspace(resource); + + autoSelection.verifyAll(); + appDiagnostics.verifyAll(); + }); + + test('If doc opened is not python, return', async () => { + const doc = { + uri: Uri.parse('doc'), + languageId: 'NOT PYTHON', + }; + + managerTest.onDocOpened((doc as 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, + }; + when(workspaceService.getWorkspaceFolderIdentifier(doc.uri, anything())).thenReturn(''); + + managerTest.onDocOpened((doc as unknown) as TextDocument); + + verify(workspaceService.getWorkspaceFolderIdentifier(doc.uri, anything())).once(); + 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, + }; + when(workspaceService.getWorkspaceFolderIdentifier(doc.uri, anything())).thenReturn('key'); + managerTest.activatedWorkspaces.add('key'); + + managerTest.onDocOpened((doc as unknown) as TextDocument); + + verify(workspaceService.getWorkspaceFolderIdentifier(doc.uri, anything())).once(); + verify(workspaceService.getWorkspaceFolder(doc.uri)).never(); + }); + + test('List of activated workspaces is updated & Handler docOpenedHandler is disposed in case no. of workspace folders decreases to one', async () => { + const disposable1 = typemoq.Mock.ofType<IDisposable>(); + const disposable2 = typemoq.Mock.ofType<IDisposable>(); + let docOpenedHandler!: (e: TextDocument) => Promise<void>; + // eslint-disable-next-line @typescript-eslint/ban-types + let workspaceFoldersChangedHandler!: Function; + const documentUri = Uri.file('a'); + const document = typemoq.Mock.ofType<TextDocument>(); + document.setup((d) => d.uri).returns(() => documentUri); + + when(workspaceService.onDidChangeWorkspaceFolders).thenReturn((cb) => { + workspaceFoldersChangedHandler = cb; + return disposable1.object; + }); + documentManager + .setup((w) => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((cb) => { + docOpenedHandler = cb; + }) + .returns(() => disposable2.object) + .verifiable(typemoq.Times.once()); + + const resource = Uri.parse('two'); + const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; + const folder2 = { name: 'two', uri: resource, index: 2 }; + when(workspaceService.workspaceFolders).thenReturn([folder1, folder2]); + + when(workspaceService.getWorkspaceFolderIdentifier(folder1.uri, anything())).thenReturn('one'); + when(workspaceService.getWorkspaceFolderIdentifier(folder2.uri, anything())).thenReturn('two'); + // Assume the two workspaces are already activated, so their keys will be present in `activatedWorkspaces` set + managerTest.activatedWorkspaces.add('one'); + managerTest.activatedWorkspaces.add('two'); + + // Add workspaceFoldersChangedHandler + managerTest.addHandlers(); + expect(workspaceFoldersChangedHandler).not.to.be.equal(undefined, 'Handler not set'); + + // Add docOpenedHandler + workspaceFoldersChangedHandler.call(managerTest); + expect(docOpenedHandler).not.to.be.equal(undefined, 'Handler not set'); + + documentManager.verifyAll(); + verify(workspaceService.onDidChangeWorkspaceFolders).once(); + verify(workspaceService.workspaceFolders).atLeast(1); + + // Removed no. of folders to one + when(workspaceService.workspaceFolders).thenReturn([folder1]); + disposable2.setup((d) => d.dispose()).verifiable(typemoq.Times.once()); + + workspaceFoldersChangedHandler.call(managerTest); + + verify(workspaceService.workspaceFolders).atLeast(1); + disposable2.verifyAll(); + + assert.deepEqual(Array.from(managerTest.activatedWorkspaces.keys()), ['one']); + }); + }); +}); diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts deleted file mode 100644 index cff248783b3d..000000000000 --- a/src/test/activation/activationService.unit.test.ts +++ /dev/null @@ -1,279 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-func-body-length - -import { SemVer } from 'semver'; -import * as TypeMoq from 'typemoq'; -import { ConfigurationChangeEvent, Disposable } from 'vscode'; -import { ExtensionActivationService } from '../../client/activation/activationService'; -import { - ExtensionActivators, FolderVersionPair, - IExtensionActivationService, IExtensionActivator, - ILanguageServerCompatibilityService, - ILanguageServerFolderService -} from '../../client/activation/types'; -import { LSNotSupportedDiagnosticServiceId } from '../../client/application/diagnostics/checks/lsNotSupported'; -import { IDiagnosticsService } from '../../client/application/diagnostics/types'; -import { - IApplicationShell, ICommandManager, - IWorkspaceService -} from '../../client/common/application/types'; -import { IPlatformService } from '../../client/common/platform/types'; -import { - IConfigurationService, IDisposableRegistry, - IOutputChannel, IPythonSettings -} from '../../client/common/types'; -import { IServiceContainer } from '../../client/ioc/types'; - -suite('Activation - ActivationService', () => { - [true, false].forEach(jediIsEnabled => { - suite(`Jedi is ${jediIsEnabled ? 'enabled' : '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 lanagueServerSupportedService: TypeMoq.IMock<ILanguageServerCompatibilityService>; - let lsNotSupportedDiagnosticService: TypeMoq.IMock<IDiagnosticsService>; - 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>(); - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - const langFolderServiceMock = TypeMoq.Mock.ofType<ILanguageServerFolderService>(); - const folderVer: FolderVersionPair = { - path: '', - version: new SemVer('1.2.3') - }; - lanagueServerSupportedService = TypeMoq.Mock.ofType<ILanguageServerCompatibilityService>(); - 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)); - - 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(ILanguageServerFolderService))).returns(() => langFolderServiceMock.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<IExtensionActivator>, lsSupported: boolean = true) { - activator - .setup(a => a.activate()).returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - let activatorName = ExtensionActivators.Jedi; - if (lsSupported && !jediIsEnabled) { - activatorName = ExtensionActivators.DotNet; - } - let diagnostics; - if (!lsSupported && !jediIsEnabled) { - diagnostics = [TypeMoq.It.isAny()]; - } else{ - diagnostics = []; - } - lsNotSupportedDiagnosticService.setup(l => l.diagnose()).returns(() => diagnostics); - lsNotSupportedDiagnosticService.setup(l => l.handle(TypeMoq.It.isValue(diagnostics))).returns(() => Promise.resolve()); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IExtensionActivator), TypeMoq.It.isValue(activatorName))) - .returns(() => activator.object) - .verifiable(TypeMoq.Times.once()); - - await activationService.activate(); - - activator.verifyAll(); - serviceContainer.verifyAll(); - } - - test('LS is supported', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); - pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); - const activator = TypeMoq.Mock.ofType<IExtensionActivator>(); - const activationService = new ExtensionActivationService(serviceContainer.object); - - await testActivation(activationService, activator, true); - }); - test('LS is not supported', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(false)); - pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); - const activator = TypeMoq.Mock.ofType<IExtensionActivator>(); - const activationService = new ExtensionActivationService(serviceContainer.object); - - await testActivation(activationService, activator, false); - }); - - test('Activatory must be activated', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); - pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); - const activator = TypeMoq.Mock.ofType<IExtensionActivator>(); - const activationService = new ExtensionActivationService(serviceContainer.object); - - await testActivation(activationService, activator); - }); - test('Activatory must be deactivated', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); - pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); - const activator = TypeMoq.Mock.ofType<IExtensionActivator>(); - const activationService = new ExtensionActivationService(serviceContainer.object); - - await testActivation(activationService, activator); - - activator - .setup(a => a.deactivate()).returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - - activationService.dispose(); - activator.verifyAll(); - }); - test('Prompt user to reload VS Code and reload, when setting is toggled', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); - let callbackHandler!: (e: ConfigurationChangeEvent) => Promise<void>; - let jediIsEnabledValueInSetting = jediIsEnabled; - 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.jediEnabled).returns(() => jediIsEnabledValueInSetting); - const activator = TypeMoq.Mock.ofType<IExtensionActivator>(); - const activationService = new ExtensionActivationService(serviceContainer.object); - - workspaceService.verifyAll(); - await testActivation(activationService, activator); - - const event = TypeMoq.Mock.ofType<ConfigurationChangeEvent>(); - event.setup(e => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), 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. - jediIsEnabledValueInSetting = !jediIsEnabledValueInSetting; - await callbackHandler(event.object); - - event.verifyAll(); - appShell.verifyAll(); - cmdManager.verifyAll(); - }); - test('Prompt user to reload VS Code and do not reload, when setting is toggled', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); - let callbackHandler!: (e: ConfigurationChangeEvent) => Promise<void>; - let jediIsEnabledValueInSetting = jediIsEnabled; - 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.jediEnabled).returns(() => jediIsEnabledValueInSetting); - const activator = TypeMoq.Mock.ofType<IExtensionActivator>(); - const activationService = new ExtensionActivationService(serviceContainer.object); - - workspaceService.verifyAll(); - await testActivation(activationService, activator); - - const event = TypeMoq.Mock.ofType<ConfigurationChangeEvent>(); - event.setup(e => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isValue('Reload'))) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - cmdManager.setup(c => c.executeCommand(TypeMoq.It.isValue('workbench.action.reloadWindow'))) - .verifiable(TypeMoq.Times.never()); - - // Toggle the value in the setting and invoke the callback. - jediIsEnabledValueInSetting = !jediIsEnabledValueInSetting; - await callbackHandler(event.object); - - event.verifyAll(); - appShell.verifyAll(); - cmdManager.verifyAll(); - }); - test('Do not prompt user to reload VS Code when setting is not toggled', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); - 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.jediEnabled).returns(() => jediIsEnabled); - const activator = TypeMoq.Mock.ofType<IExtensionActivator>(); - const activationService = new ExtensionActivationService(serviceContainer.object); - - workspaceService.verifyAll(); - await testActivation(activationService, activator); - - const event = TypeMoq.Mock.ofType<ConfigurationChangeEvent>(); - event.setup(e => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), TypeMoq.It.isAny())) - .returns(() => true) - .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('Do not prompt user to reload VS Code when setting is not changed', async () => { - lanagueServerSupportedService.setup(ls => ls.isSupported()).returns(() => Promise.resolve(true)); - 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.jediEnabled).returns(() => jediIsEnabled); - const activator = TypeMoq.Mock.ofType<IExtensionActivator>(); - const activationService = new ExtensionActivationService(serviceContainer.object); - - workspaceService.verifyAll(); - await testActivation(activationService, activator); - - const event = TypeMoq.Mock.ofType<ConfigurationChangeEvent>(); - event.setup(e => e.affectsConfiguration(TypeMoq.It.isValue('python.jediEnabled'), 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(); - }); - }); - }); -}); diff --git a/src/test/activation/activeResource.unit.test.ts b/src/test/activation/activeResource.unit.test.ts new file mode 100644 index 000000000000..4b157f950bf3 --- /dev/null +++ b/src/test/activation/activeResource.unit.test.ts @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import { instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { ActiveResourceService } from '../../client/common/application/activeResource'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; + +suite('Active resource service', () => { + let documentManager: IDocumentManager; + let workspaceService: IWorkspaceService; + let activeResourceService: ActiveResourceService; + setup(() => { + documentManager = mock(DocumentManager); + workspaceService = mock(WorkspaceService); + activeResourceService = new ActiveResourceService(instance(documentManager), instance(workspaceService)); + }); + + test('Return document uri if the active document is not new (has been saved)', async () => { + const activeTextEditor = { + document: { + isUntitled: false, + uri: Uri.parse('a'), + }, + }; + + when(documentManager.activeTextEditor).thenReturn(activeTextEditor as any); + + const activeResource = activeResourceService.getActiveResource(); + + assert.deepEqual(activeResource, activeTextEditor.document.uri); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspaceService.workspaceFolders).never(); + }); + + test("Don't return document uri if the active document is new (still unsaved)", async () => { + const activeTextEditor = { + document: { + isUntitled: true, + uri: Uri.parse('a'), + }, + }; + + when(documentManager.activeTextEditor).thenReturn(activeTextEditor as any); + when(workspaceService.workspaceFolders).thenReturn([]); + + const activeResource = activeResourceService.getActiveResource(); + + assert.notDeepEqual(activeResource, activeTextEditor.document.uri); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspaceService.workspaceFolders).atLeast(1); + }); + + test('If no document is currently opened & the workspace opened contains workspace folders, return the uri of the first workspace folder', async () => { + const workspaceFolders = [ + { + uri: Uri.parse('a'), + }, + { + uri: Uri.parse('b'), + }, + ]; + when(documentManager.activeTextEditor).thenReturn(undefined); + + when(workspaceService.workspaceFolders).thenReturn(workspaceFolders as any); + + const activeResource = activeResourceService.getActiveResource(); + + assert.deepEqual(activeResource, workspaceFolders[0].uri); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspaceService.workspaceFolders).atLeast(1); + }); + + test('If no document is currently opened & no folder is opened, return undefined', async () => { + when(documentManager.activeTextEditor).thenReturn(undefined); + when(workspaceService.workspaceFolders).thenReturn(undefined); + + const activeResource = activeResourceService.getActiveResource(); + + assert.deepEqual(activeResource, undefined); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspaceService.workspaceFolders).atLeast(1); + }); + + test('If no document is currently opened & workspace contains no workspace folders, return undefined', async () => { + when(documentManager.activeTextEditor).thenReturn(undefined); + when(workspaceService.workspaceFolders).thenReturn([]); + + const activeResource = activeResourceService.getActiveResource(); + + assert.deepEqual(activeResource, undefined); + verify(documentManager.activeTextEditor).atLeast(1); + verify(workspaceService.workspaceFolders).atLeast(1); + }); +}); diff --git a/src/test/activation/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/downloadChannelRules.unit.test.ts b/src/test/activation/downloadChannelRules.unit.test.ts deleted file mode 100644 index 8477a38df77d..000000000000 --- a/src/test/activation/downloadChannelRules.unit.test.ts +++ /dev/null @@ -1,65 +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/downloadChannelRules'; -import { IPersistentState, IPersistentStateFactory } from '../../client/common/types'; -import { IServiceContainer } from '../../client/ioc/types'; - -suite('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/downloader.unit.test.ts b/src/test/activation/downloader.unit.test.ts deleted file mode 100644 index 684ddcd61592..000000000000 --- a/src/test/activation/downloader.unit.test.ts +++ /dev/null @@ -1,49 +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 TypeMoq from 'typemoq'; -import { LanguageServerDownloader } from '../../client/activation/downloader'; -import { PlatformData } from '../../client/activation/platformData'; -import { ILanguageServerFolderService } from '../../client/activation/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { IOutputChannel } from '../../client/common/types'; -import { IServiceContainer } from '../../client/ioc/types'; - -suite('Activation - Downloader', () => { - let languageServerDownloader: LanguageServerDownloader; - let platformService: TypeMoq.IMock<IPlatformService>; - let container: TypeMoq.IMock<IServiceContainer>; - let folderService: TypeMoq.IMock<ILanguageServerFolderService>; - setup(() => { - container = TypeMoq.Mock.ofType<IServiceContainer>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - folderService = TypeMoq.Mock.ofType<ILanguageServerFolderService>(); - const fs = TypeMoq.Mock.ofType<IFileSystem>(); - const output = TypeMoq.Mock.ofType<IOutputChannel>(); - const platformData: PlatformData = new PlatformData(platformService.object, fs.object); - container.setup(a => a.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(STANDARD_OUTPUT_CHANNEL))).returns(() => output.object); - container.setup(a => a.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fs.object); - container.setup(a => a.get(TypeMoq.It.isValue(ILanguageServerFolderService))).returns(() => folderService.object); - - languageServerDownloader = new LanguageServerDownloader(platformData, '', container.object); - }); - - test('Get download uri', async () => { - const pkg = { uri: 'xyz' } as any; - folderService - .setup(f => f.getLatestLanguageServerVersion()) - .returns(() => Promise.resolve(pkg)) - .verifiable(TypeMoq.Times.once()); - - const info = await languageServerDownloader.getDownloadInfo(); - - folderService.verifyAll(); - expect(info).to.deep.equal(pkg); - }); -}); diff --git a/src/test/activation/extensionSurvey.unit.test.ts b/src/test/activation/extensionSurvey.unit.test.ts new file mode 100644 index 000000000000..a89797bfebef --- /dev/null +++ b/src/test/activation/extensionSurvey.unit.test.ts @@ -0,0 +1,561 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { ExtensionSurveyPrompt, extensionSurveyStateKeys } from '../../client/activation/extensionSurvey'; +import { IApplicationEnvironment, IApplicationShell, 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, + IExperimentService, + IPersistentState, + IPersistentStateFactory, + IRandom, +} from '../../client/common/types'; +import { createDeferred } from '../../client/common/utils/async'; +import { Common, ExtensionSurveyBanner } from '../../client/common/utils/localize'; +import { OSType } from '../../client/common/utils/platform'; +import { sleep } from '../core'; +import { WorkspaceConfiguration } from 'vscode'; + +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<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<IExperimentService>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + browserService = TypeMoq.Mock.ofType<IBrowserService>(); + random = TypeMoq.Mock.ofType<IRandom>(); + persistentStateFactory = mock(PersistentStateFactory); + disableSurveyForTime = TypeMoq.Mock.ofType<IPersistentState<any>>(); + doNotShowAgain = TypeMoq.Mock.ofType<IPersistentState<any>>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + appEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + when( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything(), + ), + ).thenReturn(disableSurveyForTime.object); + when( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), + ).thenReturn(doNotShowAgain.object); + extensionSurveyPrompt = new ExtensionSurveyPrompt( + appShell.object, + browserService.object, + instance(persistentStateFactory), + random.object, + experiments.object, + appEnvironment.object, + platformService.object, + workspaceService.object, + 10, + ); + }); + test('Returns false if do not show again is clicked', async () => { + random + .setup((r) => r.getRandomInt(0, 100)) + .returns(() => 10) + .verifiable(TypeMoq.Times.never()); + doNotShowAgain.setup((d) => d.value).returns(() => true); + + const result = extensionSurveyPrompt.shouldShowBanner(); + + expect(result).to.equal(false, 'Banner should not be shown'); + verify( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything(), + ), + ).never(); + verify( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), + ).once(); + random.verifyAll(); + }); + test('Returns false if prompt is disabled for a while', async () => { + random + .setup((r) => r.getRandomInt(0, 100)) + .returns(() => 10) + .verifiable(TypeMoq.Times.never()); + disableSurveyForTime.setup((d) => d.value).returns(() => true); + doNotShowAgain.setup((d) => d.value).returns(() => false); + + const result = extensionSurveyPrompt.shouldShowBanner(); + + expect(result).to.equal(false, 'Banner should not be shown'); + verify( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything(), + ), + ).once(); + verify( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), + ).once(); + random.verifyAll(); + }); + test('Returns false if user is not in the random sampling', async () => { + disableSurveyForTime.setup((d) => d.value).returns(() => false); + doNotShowAgain.setup((d) => d.value).returns(() => false); + // Default sample size is 10 + for (let i = 10; i < 100; i = i + 1) { + random.setup((r) => r.getRandomInt(0, 100)).returns(() => i); + const result = extensionSurveyPrompt.shouldShowBanner(); + expect(result).to.equal(false, 'Banner should not be shown'); + } + random.verifyAll(); + }); + test('Returns true if 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); + // Default sample size is 10 + for (let i = 0; i < 10; i = i + 1) { + random.setup((r) => r.getRandomInt(0, 100)).returns(() => i); + const result = extensionSurveyPrompt.shouldShowBanner(); + expect(result).to.equal(true, 'Banner should be shown'); + } + }); + + test('Always return true if sample size is 100', async () => { + extensionSurveyPrompt = new ExtensionSurveyPrompt( + appShell.object, + browserService.object, + instance(persistentStateFactory), + random.object, + experiments.object, + appEnvironment.object, + platformService.object, + workspaceService.object, + 100, + ); + disableSurveyForTime.setup((d) => d.value).returns(() => false); + doNotShowAgain.setup((d) => d.value).returns(() => false); + for (let i = 0; i < 100; i = i + 1) { + random.setup((r) => r.getRandomInt(0, 100)).returns(() => i); + const result = extensionSurveyPrompt.shouldShowBanner(); + expect(result).to.equal(true, 'Banner should be shown'); + } + }); + + test('Always return false if sample size is 0', async () => { + extensionSurveyPrompt = new ExtensionSurveyPrompt( + appShell.object, + browserService.object, + instance(persistentStateFactory), + random.object, + experiments.object, + appEnvironment.object, + platformService.object, + workspaceService.object, + 0, + ); + disableSurveyForTime.setup((d) => d.value).returns(() => false); + doNotShowAgain.setup((d) => d.value).returns(() => false); + for (let i = 0; i < 100; i = i + 1) { + random.setup((r) => r.getRandomInt(0, 100)).returns(() => i); + const result = extensionSurveyPrompt.shouldShowBanner(); + expect(result).to.equal(false, 'Banner should not be shown'); + } + random.verifyAll(); + }); +}); + +suite('Extension survey prompt - showSurvey()', () => { + let experiments: TypeMoq.IMock<IExperimentService>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let browserService: TypeMoq.IMock<IBrowserService>; + let random: TypeMoq.IMock<IRandom>; + let persistentStateFactory: IPersistentStateFactory; + let disableSurveyForTime: TypeMoq.IMock<IPersistentState<any>>; + let doNotShowAgain: TypeMoq.IMock<IPersistentState<any>>; + let platformService: TypeMoq.IMock<IPlatformService>; + let appEnvironment: TypeMoq.IMock<IApplicationEnvironment>; + let extensionSurveyPrompt: ExtensionSurveyPrompt; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + setup(() => { + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + browserService = TypeMoq.Mock.ofType<IBrowserService>(); + random = TypeMoq.Mock.ofType<IRandom>(); + persistentStateFactory = mock(PersistentStateFactory); + disableSurveyForTime = TypeMoq.Mock.ofType<IPersistentState<any>>(); + doNotShowAgain = TypeMoq.Mock.ofType<IPersistentState<any>>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + appEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + when( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything(), + ), + ).thenReturn(disableSurveyForTime.object); + when( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), + ).thenReturn(doNotShowAgain.object); + experiments = TypeMoq.Mock.ofType<IExperimentService>(); + extensionSurveyPrompt = new ExtensionSurveyPrompt( + appShell.object, + browserService.object, + instance(persistentStateFactory), + random.object, + experiments.object, + appEnvironment.object, + platformService.object, + workspaceService.object, + 10, + ); + }); + + test("Launch survey if 'Yes' option is clicked", async () => { + const packageJson = { + version: 'extensionVersion', + }; + const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain]; + const expectedUrl = `https://aka.ms/AA5rjx5?o=Windows&v=vscodeVersion&e=extensionVersion&m=sessionId`; + appEnvironment + .setup((a) => a.packageJson) + .returns(() => packageJson) + .verifiable(TypeMoq.Times.once()); + appEnvironment + .setup((a) => a.vscodeVersion) + .returns(() => 'vscodeVersion') + .verifiable(TypeMoq.Times.once()); + appEnvironment + .setup((a) => a.sessionId) + .returns(() => 'sessionId') + .verifiable(TypeMoq.Times.once()); + platformService + .setup((a) => a.osType) + .returns(() => OSType.Windows) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage, ...prompts)) + .returns(() => Promise.resolve(ExtensionSurveyBanner.bannerLabelYes)) + .verifiable(TypeMoq.Times.once()); + browserService + .setup((s) => s.launch(expectedUrl)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + disableSurveyForTime + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + doNotShowAgain + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await extensionSurveyPrompt.showSurvey(); + + verify( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything(), + ), + ).once(); + verify( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), + ).never(); + appShell.verifyAll(); + browserService.verifyAll(); + disableSurveyForTime.verifyAll(); + doNotShowAgain.verifyAll(); + appEnvironment.verifyAll(); + platformService.verifyAll(); + }); + + test("Do nothing if 'Maybe later' option is clicked", async () => { + const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain]; + platformService.setup((p) => p.osType).verifiable(TypeMoq.Times.never()); + appShell + .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage, ...prompts)) + .returns(() => Promise.resolve(ExtensionSurveyBanner.maybeLater)) + .verifiable(TypeMoq.Times.once()); + browserService + .setup((s) => s.launch(TypeMoq.It.isAny())) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + disableSurveyForTime + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + doNotShowAgain + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await extensionSurveyPrompt.showSurvey(); + + verify( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything(), + ), + ).never(); + verify( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), + ).never(); + appShell.verifyAll(); + browserService.verifyAll(); + disableSurveyForTime.verifyAll(); + doNotShowAgain.verifyAll(); + platformService.verifyAll(); + }); + + test('Do nothing if no option is clicked', async () => { + const prompts = [ExtensionSurveyBanner.bannerLabelYes, ExtensionSurveyBanner.maybeLater, Common.doNotShowAgain]; + platformService.setup((p) => p.osType).verifiable(TypeMoq.Times.never()); + appShell + .setup((a) => a.showInformationMessage(ExtensionSurveyBanner.bannerMessage, ...prompts)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + browserService + .setup((s) => s.launch(TypeMoq.It.isAny())) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + disableSurveyForTime + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + doNotShowAgain + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await extensionSurveyPrompt.showSurvey(); + + verify( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything(), + ), + ).never(); + verify( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), + ).never(); + appShell.verifyAll(); + browserService.verifyAll(); + disableSurveyForTime.verifyAll(); + doNotShowAgain.verifyAll(); + platformService.verifyAll(); + }); + + test('Disable prompt if "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)) + .verifiable(TypeMoq.Times.once()); + browserService + .setup((s) => s.launch(TypeMoq.It.isAny())) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + disableSurveyForTime + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + doNotShowAgain + .setup((d) => d.updateValue(true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await extensionSurveyPrompt.showSurvey(); + + verify( + persistentStateFactory.createGlobalPersistentState( + extensionSurveyStateKeys.disableSurveyForTime, + false, + anything(), + ), + ).never(); + verify( + persistentStateFactory.createGlobalPersistentState(extensionSurveyStateKeys.doNotShowAgain, false), + ).once(); + appShell.verifyAll(); + browserService.verifyAll(); + disableSurveyForTime.verifyAll(); + doNotShowAgain.verifyAll(); + platformService.verifyAll(); + }); +}); + +suite('Extension survey prompt - activate()', () => { + let appShell: TypeMoq.IMock<IApplicationShell>; + let browserService: TypeMoq.IMock<IBrowserService>; + let random: TypeMoq.IMock<IRandom>; + let persistentStateFactory: IPersistentStateFactory; + let shouldShowBanner: sinon.SinonStub<any>; + let showSurvey: sinon.SinonStub<any>; + let experiments: TypeMoq.IMock<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<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 'ShowExtensionSurveyPrompt' experiment, return immediately", async () => { + shouldShowBanner = sinon.stub(ExtensionSurveyPrompt.prototype, 'shouldShowBanner'); + shouldShowBanner.callsFake(() => false); + extensionSurveyPrompt = new ExtensionSurveyPrompt( + appShell.object, + browserService.object, + instance(persistentStateFactory), + random.object, + experiments.object, + appEnvironment.object, + platformService.object, + workspaceService.object, + 10, + ); + experiments + .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 'ShowExtensionSurveyPrompt' experiment", async () => { + const deferred = createDeferred<true>(); + shouldShowBanner = sinon.stub(ExtensionSurveyPrompt.prototype, 'shouldShowBanner'); + shouldShowBanner.callsFake(() => false); + showSurvey = sinon.stub(ExtensionSurveyPrompt.prototype, 'showSurvey'); + showSurvey.callsFake(() => { + deferred.resolve(true); + return Promise.resolve(); + }); + // waitTimeToShowSurvey = 50 ms + extensionSurveyPrompt = new ExtensionSurveyPrompt( + appShell.object, + browserService.object, + instance(persistentStateFactory), + random.object, + experiments.object, + appEnvironment.object, + platformService.object, + workspaceService.object, + 10, + 50, + ); + experiments + .setup((exp) => exp.inExperiment(ShowExtensionSurveyPrompt.experiment)) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + await extensionSurveyPrompt.activate(); + assert.ok(shouldShowBanner.calledOnce); + + const doesSurveyShowUp = await Promise.race([deferred.promise, sleep(100).then(() => false)]); + assert.ok(showSurvey.notCalled); + expect(doesSurveyShowUp).to.equal(false, 'Survey should not appear'); + experiments.verifyAll(); + }); + + test("Survey is shown after waitTimeToShowSurvey if shouldShowBanner() returns true and user is in 'ShowExtensionSurveyPrompt' experiment", async () => { + const deferred = createDeferred<true>(); + shouldShowBanner = sinon.stub(ExtensionSurveyPrompt.prototype, 'shouldShowBanner'); + shouldShowBanner.callsFake(() => true); + showSurvey = sinon.stub(ExtensionSurveyPrompt.prototype, 'showSurvey'); + showSurvey.callsFake(() => { + deferred.resolve(true); + return Promise.resolve(); + }); + // waitTimeToShowSurvey = 50 ms + extensionSurveyPrompt = new ExtensionSurveyPrompt( + appShell.object, + browserService.object, + instance(persistentStateFactory), + random.object, + experiments.object, + appEnvironment.object, + platformService.object, + workspaceService.object, + 10, + 50, + ); + experiments + .setup((exp) => exp.inExperiment(ShowExtensionSurveyPrompt.experiment)) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + await extensionSurveyPrompt.activate(); + assert.ok(shouldShowBanner.calledOnce); + + const doesSurveyShowUp = await Promise.race([deferred.promise, sleep(200).then(() => false)]); + expect(doesSurveyShowUp).to.equal(true, 'Survey should appear'); + assert.ok(showSurvey.calledOnce); + experiments.verifyAll(); + }); +}); diff --git a/src/test/activation/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/languageServer.unit.test.ts b/src/test/activation/languageServer/languageServer.unit.test.ts deleted file mode 100644 index 492ccfc9e4da..000000000000 --- a/src/test/activation/languageServer/languageServer.unit.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 - -import { expect } from 'chai'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { LanguageServerExtensionActivator } from '../../../client/activation/languageServer/languageServer'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../client/common/application/types'; -import { IPlatformService } from '../../../client/common/platform/types'; -import { IConfigurationService, IDisposableRegistry, IExtensionContext, IFeatureDeprecationManager, IOutputChannel, IPathUtils, IPythonSettings } from '../../../client/common/types'; -import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; -import { IServiceContainer } from '../../../client/ioc/types'; - -suite('Language Server', () => { - 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 languageServer: LanguageServerExtensionActivator; - let extensionContext: TypeMoq.IMock<IExtensionContext>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - extensionContext = TypeMoq.Mock.ofType<IExtensionContext>(); - appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - cmdManager = TypeMoq.Mock.ofType<ICommandManager>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - - workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); - workspaceService.setup(w => w.workspaceFolders).returns(() => []); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.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(IExtensionContext))).returns(() => extensionContext.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFeatureDeprecationManager))).returns(() => TypeMoq.Mock.ofType<IFeatureDeprecationManager>().object); - - languageServer = new LanguageServerExtensionActivator(serviceContainer.object); - }); - - test('Must get PYTHONPATH from env vars provider', async () => { - const pathDelimiter = 'x'; - const pythonPathVar = ['A', 'B', '1']; - const envVarsProvider = TypeMoq.Mock.ofType<IEnvironmentVariablesProvider>(); - const pathUtils = TypeMoq.Mock.ofType<IPathUtils>(); - extensionContext.setup(e => e.extensionPath).returns(() => path.join('a', 'b', 'c')); - pathUtils.setup(p => p.delimiter).returns(() => pathDelimiter); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider))).returns(() => envVarsProvider.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); - envVarsProvider - .setup(p => p.getEnvironmentVariables()) - .returns(() => { return Promise.resolve({ PYTHONPATH: pythonPathVar.join(pathDelimiter) }); }) - .verifiable(TypeMoq.Times.once()); - - // tslint:disable-next-line:no-any - (languageServer as any).languageServerFolder = ''; - const options = await languageServer.getAnalysisOptions(); - - expect(options!).not.to.equal(undefined, 'options cannot be undefined'); - expect(options!.initializationOptions).not.to.equal(undefined, 'initializationOptions cannot be undefined'); - expect(options!.initializationOptions!.searchPaths).to.include.members(pythonPathVar); - }); -}); 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 92d1ac7895b7..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/languageServerPackageRepository.unit.test.ts b/src/test/activation/languageServer/languageServerPackageRepository.unit.test.ts deleted file mode 100644 index 6121dd59f46e..000000000000 --- a/src/test/activation/languageServer/languageServerPackageRepository.unit.test.ts +++ /dev/null @@ -1,53 +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 { BetaLanguageServerPackageRepository, DailyLanguageServerPackageRepository, LanguageServerDownloadChannel, StableLanguageServerPackageRepository } 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 = StableLanguageServerPackageRepository; - switch (channel) { - case LanguageServerDownloadChannel.stable: { - classToCreate = StableLanguageServerPackageRepository; - break; - } - case LanguageServerDownloadChannel.beta: { - classToCreate = BetaLanguageServerPackageRepository; - break; - } - case LanguageServerDownloadChannel.daily: { - classToCreate = DailyLanguageServerPackageRepository; - 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 6c5e1b3ea85c..000000000000 --- a/src/test/activation/languageServer/languageServerPackageService.test.ts +++ /dev/null @@ -1,53 +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 { LanguageServerPackageStorageContainers } from '../../../client/activation/languageServer/languageServerPackageRepository'; -import { LanguageServerPackageService } from '../../../client/activation/languageServer/languageServerPackageService'; -import { IApplicationEnvironment } 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 defaultStorageChannel = LanguageServerPackageStorageContainers.stable; - 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 = { languageServerVersion: '0.1.0' }; - appEnv.setup(e => e.packageJson).returns(() => packageJson); - const platform = typeMoq.Mock.ofType<IPlatformService>(); - const lsPackageService = new LanguageServerPackageService(serviceContainer.object, appEnv.object, platform.object); - const packageName = lsPackageService.getNugetPackageName(); - const packages = await nugetRepo.getPackages(packageName); - - 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 1320ff0baa59..000000000000 --- a/src/test/activation/languageServer/languageServerPackageService.unit.test.ts +++ /dev/null @@ -1,159 +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, LanguageServerPackageStorageContainers } from '../../../client/activation/languageServer/languageServerPackageRepository'; -import { LanguageServerPackageService } from '../../../client/activation/languageServer/languageServerPackageService'; -import { PlatformName } from '../../../client/activation/platformData'; -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 { OSType } from '../../../client/common/utils/platform'; -import { IServiceContainer } from '../../../client/ioc/types'; - -const downloadBaseFileName = 'Python-Language-Server'; - -suite('Languagex', () => { - let serviceContainer: typeMoq.IMock<IServiceContainer>; - let platform: typeMoq.IMock<IPlatformService>; - let lsPackageService: LanguageServerPackageService; - let appVersion: typeMoq.IMock<IApplicationEnvironment>; - setup(() => { - serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); - platform = typeMoq.Mock.ofType<IPlatformService>(); - appVersion = typeMoq.Mock.ofType<IApplicationEnvironment>(); - lsPackageService = new LanguageServerPackageService(serviceContainer.object, appVersion.object, platform.object); - lsPackageService.getLanguageServerDownloadChannel = () => 'stable'; - }); - function setMinVersionOfLs(version: string) { - const packageJson = { languageServerVersion: 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))) - .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(); - - 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))) - .returns(() => Promise.resolve(packages)) - .verifiable(typeMoq.Times.once()); - - const info = await lsPackageService.getLatestNugetPackageVersion(); - - 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))) - .returns(() => Promise.resolve(packages)) - .verifiable(typeMoq.Times.once()); - - const info = await lsPackageService.getLatestNugetPackageVersion(); - - repo.verifyAll(); - const expectedPackage: NugetPackage = { - version: new SemVer(minimumVersion), - package: LanguageServerPackageStorageContainers.stable, - uri: `${azureCDNBlobStorageAccount}/${LanguageServerPackageStorageContainers.stable}/${packageName}.${minimumVersion}.nupkg` - }; - expect(info).to.deep.equal(expectedPackage); - }); -}); 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 new file mode 100644 index 000000000000..7f1dffaf848b --- /dev/null +++ b/src/test/activation/node/languageServerChangeHandler.unit.test.ts @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +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 { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../client/common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../../../client/common/constants'; +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 commands: ICommandManager; + let extensionsChangedEvent: EventEmitter<void>; + let handler: LanguageServerChangeHandler; + + let workspace: IWorkspaceService; + let configService: IConfigurationService; + + setup(() => { + extensions = mock<IExtensions>(); + appShell = mock<IApplicationShell>(); + commands = mock<ICommandManager>(); + workspace = mock<IWorkspaceService>(); + configService = mock<IConfigurationService>(); + + extensionsChangedEvent = new EventEmitter<void>(); + when(extensions.onDidChange).thenReturn(extensionsChangedEvent.event); + }); + teardown(() => { + extensionsChangedEvent.dispose(); + handler?.dispose(); + }); + + [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.showInformationMessage(anyString(), anyString())).never(); + verify(appShell.showWarningMessage(anyString(), anyString())).never(); + verify(commands.executeCommand(anyString())).never(); + }); + }); + + test('Handler should prompt for install when language server changes to Pylance and Pylance is not installed', async () => { + when( + appShell.showWarningMessage( + 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.showWarningMessage( + 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.pylanceRevertToJediPrompt, + Pylance.pylanceInstallPylance, + Pylance.pylanceRevertToJedi, + Pylance.remindMeLater, + ), + ).thenReturn(Promise.resolve(Pylance.pylanceInstallPylance)); + + handler = makeHandler(undefined); + await handler.handleLanguageServerChange(LanguageServerType.Node); + + 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.pylanceRevertToJediPrompt, + Pylance.pylanceInstallPylance, + Pylance.pylanceRevertToJedi, + Pylance.remindMeLater, + ), + ).thenReturn(Promise.resolve(Pylance.remindMeLater)); + + handler = makeHandler(undefined); + await handler.handleLanguageServerChange(LanguageServerType.Node); + + verify(commands.executeCommand('extension.open', PYLANCE_EXTENSION_ID)).never(); + verify(commands.executeCommand('workbench.action.reloadWindow')).never(); + }); + + [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(); + }); + }); + + [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( + 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', + 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 { + return new LanguageServerChangeHandler( + initialLSType, + instance(extensions), + instance(appShell), + instance(commands), + instance(workspace), + instance(configService), + ); + } +}); diff --git a/src/test/activation/outputChannel.unit.test.ts b/src/test/activation/outputChannel.unit.test.ts new file mode 100644 index 000000000000..f8f38783bb0e --- /dev/null +++ b/src/test/activation/outputChannel.unit.test.ts @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { LanguageServerOutputChannel } from '../../client/activation/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'; + +suite('Language Server Output Channel', () => { + let appShell: TypeMoq.IMock<IApplicationShell>; + let languageServerOutputChannel: LanguageServerOutputChannel; + let commandManager: TypeMoq.IMock<ICommandManager>; + let output: TypeMoq.IMock<ILogOutputChannel>; + setup(() => { + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + output = TypeMoq.Mock.ofType<ILogOutputChannel>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + languageServerOutputChannel = new LanguageServerOutputChannel(appShell.object, commandManager.object, []); + }); + + test('Create output channel if one does not exist before and return it', async () => { + appShell + .setup((a) => a.createOutputChannel(OutputChannelNames.languageServer)) + .returns(() => output.object) + .verifiable(TypeMoq.Times.once()); + const { channel } = languageServerOutputChannel; + appShell.verifyAll(); + expect(channel).to.not.equal(undefined, 'Channel should not be undefined'); + }); + + test('Do not create output channel if one already exists', async () => { + languageServerOutputChannel.output = output.object; + appShell + .setup((a) => a.createOutputChannel(TypeMoq.It.isAny())) + .returns(() => output.object) + .verifiable(TypeMoq.Times.never()); + const { channel } = languageServerOutputChannel; + appShell.verifyAll(); + expect(channel).to.not.equal(undefined, 'Channel should not be undefined'); + }); + test('Register Command to display output panel', async () => { + appShell + .setup((a) => a.createOutputChannel(TypeMoq.It.isAny())) + .returns(() => output.object) + .verifiable(TypeMoq.Times.once()); + commandManager + .setup((c) => + c.executeCommand( + TypeMoq.It.isValue('setContext'), + TypeMoq.It.isValue('python.hasLanguageServerOutputChannel'), + TypeMoq.It.isValue(true), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + commandManager + .setup((c) => c.registerCommand(TypeMoq.It.isValue('python.viewLanguageServerOutput'), TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.once()); + + // Doesn't matter how many times we access channel property. + let { channel } = languageServerOutputChannel; + channel = languageServerOutputChannel.channel; + channel = languageServerOutputChannel.channel; + + await sleep(1); + + appShell.verifyAll(); + commandManager.verifyAll(); + expect(channel).to.not.equal(undefined, 'Channel should not be undefined'); + }); + test('Display panel when invoking command python.viewLanguageServerOutput', async () => { + let cmdCallback: () => unknown | undefined = () => { + /* no-op */ + }; + appShell + .setup((a) => a.createOutputChannel(TypeMoq.It.isAny())) + .returns(() => output.object) + .verifiable(TypeMoq.Times.once()); + commandManager + .setup((c) => + c.executeCommand( + TypeMoq.It.isValue('setContext'), + TypeMoq.It.isValue('python.hasLanguageServerOutputChannel'), + TypeMoq.It.isValue(true), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + commandManager + .setup((c) => c.registerCommand(TypeMoq.It.isValue('python.viewLanguageServerOutput'), TypeMoq.It.isAny())) + .callback((_: string, callback: () => 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 property. + let { channel } = languageServerOutputChannel; + channel = languageServerOutputChannel.channel; + channel = languageServerOutputChannel.channel; + + await sleep(1); + + appShell.verifyAll(); + commandManager.verifyAll(); + output.verifyAll(); + expect(channel).to.not.equal(undefined, 'Channel should not be undefined'); + expect(cmdCallback).to.not.equal(undefined, 'Command handler should not be undefined'); + + // Confirm panel is displayed when command handler is invoked. + output.reset(); + output.setup((o) => o.show(true)).verifiable(TypeMoq.Times.once()); + + // Invoke callback. + cmdCallback!(); + + output.verifyAll(); + }); +}); diff --git a/src/test/activation/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/platformData.unit.test.ts b/src/test/activation/platformData.unit.test.ts deleted file mode 100644 index 0044c27b0182..000000000000 --- a/src/test/activation/platformData.unit.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// tslint:disable:no-unused-variable -import * as assert from 'assert'; -import * as TypeMoq from 'typemoq'; -import { PlatformData, PlatformLSExecutables } from '../../client/activation/platformData'; -import { IFileSystem, 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('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 fs = TypeMoq.Mock.ofType<IFileSystem>(); - const pd = new PlatformData(platformService.object, fs.object); - - const actual = await pd.getPlatformName(); - assert.equal(actual, t.expectedName, `${actual} does not match ${t.expectedName}`); - - const actualHash = await pd.getExpectedHash(); - assert.equal(actualHash, t.expectedName, `${actual} hash 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 fs = TypeMoq.Mock.ofType<IFileSystem>(); - fs.setup(x => x.readFile(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(`NAME="name"\nID=${t.name}\nID_LIKE=debian`)); - const pd = new PlatformData(platformService.object, fs.object); - - const actual = await pd.getPlatformName(); - assert.equal(actual, t.expectedName, `${actual} does not match ${t.expectedName}`); - - const actualHash = await pd.getExpectedHash(); - assert.equal(actual, t.expectedName, `${actual} hash 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 fs = TypeMoq.Mock.ofType<IFileSystem>(); - const pd = new PlatformData(platformService.object, fs.object); - - const actual = pd.getEngineExecutableName(); - assert.equal(actual, t.expectedName, `${actual} does not match ${t.expectedName}`); - } - }); -}); 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 new file mode 100644 index 000000000000..177eae810810 --- /dev/null +++ b/src/test/activation/serviceRegistry.unit.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { instance, mock, verify } from 'ts-mockito'; + +import { ExtensionActivationManager } from '../../client/activation/activationManager'; +import { ExtensionSurveyPrompt } from '../../client/activation/extensionSurvey'; +import { LanguageServerOutputChannel } from '../../client/activation/common/outputChannel'; +import { registerTypes } from '../../client/activation/serviceRegistry'; +import { + IExtensionActivationManager, + IExtensionSingleActivationService, + ILanguageServerOutputChannel, +} from '../../client/activation/types'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { IServiceManager } from '../../client/ioc/types'; +import { LoadLanguageServerExtension } from '../../client/activation/common/loadLanguageServerExtension'; +import { RequirementsTxtLinkActivator } from '../../client/activation/requirementsTxtLinkActivator'; + +suite('Unit Tests - Language Server Activation Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure common services are registered', async () => { + registerTypes(instance(serviceManager)); + + verify( + serviceManager.add<IExtensionActivationManager>(IExtensionActivationManager, ExtensionActivationManager), + ).once(); + verify( + serviceManager.addSingleton<ILanguageServerOutputChannel>( + ILanguageServerOutputChannel, + LanguageServerOutputChannel, + ), + ).once(); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + ExtensionSurveyPrompt, + ), + ).once(); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + LoadLanguageServerExtension, + ), + ).once(); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + RequirementsTxtLinkActivator, + ), + ).once(); + }); +}); diff --git a/src/test/analysisEngineTest.ts b/src/test/analysisEngineTest.ts index acd8739ae9a5..90e433f91647 100644 --- a/src/test/analysisEngineTest.ts +++ b/src/test/analysisEngineTest.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// tslint:disable:no-console no-require-imports no-var-requires +'use strict'; + import * as path from 'path'; process.env.CODE_TESTS_WORKSPACE = path.join(__dirname, '..', '..', 'src', 'test'); @@ -11,7 +12,7 @@ process.env.TEST_FILES_SUFFIX = 'ls.test'; function start() { console.log('*'.repeat(100)); - console.log('Start Language Server tests'); + console.log('Start language server tests'); require('../../node_modules/vscode/bin/test'); } start(); diff --git a/src/test/api.functional.test.ts b/src/test/api.functional.test.ts new file mode 100644 index 000000000000..03016956dbef --- /dev/null +++ b/src/test/api.functional.test.ts @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { instance, mock, when } from 'ts-mockito'; +import { buildApi } from '../client/api'; +import { ConfigurationService } from '../client/common/configuration/service'; +import { EXTENSION_ROOT_DIR } from '../client/common/constants'; +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, 'python_files', 'lib', 'python', 'debugpy'); + const debuggerHost = 'somehost'; + const debuggerPort = 12345; + + let serviceContainer: IServiceContainer; + 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), + ); + 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); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Test debug launcher args (no-wait)', async () => { + const waitForAttach = false; + + const args = await buildApi( + Promise.resolve(), + instance(serviceManager), + instance(serviceContainer), + instance(discoverAPI), + ).debug.getRemoteLauncherCommand(debuggerHost, debuggerPort, waitForAttach); + const expectedArgs = [ + debuggerPath.fileToCommandArgumentForPythonExt(), + '--listen', + `${debuggerHost}:${debuggerPort}`, + ]; + + expect(args).to.be.deep.equal(expectedArgs); + }); + + test('Test debug launcher args (wait)', async () => { + const waitForAttach = true; + + const args = await buildApi( + Promise.resolve(), + instance(serviceManager), + instance(serviceContainer), + instance(discoverAPI), + ).debug.getRemoteLauncherCommand(debuggerHost, debuggerPort, waitForAttach); + const expectedArgs = [ + debuggerPath.fileToCommandArgumentForPythonExt(), + '--listen', + `${debuggerHost}:${debuggerPort}`, + '--wait-for-client', + ]; + + expect(args).to.be.deep.equal(expectedArgs); + }); + + test('Test debugger package path', async () => { + const pkgPath = await buildApi( + Promise.resolve(), + instance(serviceManager), + instance(serviceContainer), + instance(discoverAPI), + ).debug.getDebuggerPackagePath(); + + 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 415a34a7caf8..3a2b9c2f62dd 100644 --- a/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts +++ b/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts @@ -3,136 +3,240 @@ 'use strict'; -// tslint:disable:insecure-random - +import * as assert from 'assert'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; import { DiagnosticSeverity } from 'vscode'; import { ApplicationDiagnostics } from '../../../client/application/diagnostics/applicationDiagnostics'; -import { DiagnosticScope, IDiagnostic, IDiagnosticsService, ISourceMapSupportService } from '../../../client/application/diagnostics/types'; +import { EnvironmentPathVariableDiagnosticsService } from '../../../client/application/diagnostics/checks/envPathVariable'; +import { InvalidPythonInterpreterService } from '../../../client/application/diagnostics/checks/pythonInterpreter'; +import { DiagnosticScope, IDiagnostic, IDiagnosticsService } from '../../../client/application/diagnostics/types'; import { IApplicationDiagnostics } from '../../../client/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../../../client/common/constants'; -import { ILogger, 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 debuggerTypeCheck: typemoq.IMock<IDiagnosticsService>; - let outputChannel: typemoq.IMock<IOutputChannel>; - let logger: typemoq.IMock<ILogger>; + let lsNotSupportedCheck: typemoq.IMock<IDiagnosticsService>; + let pythonInterpreterCheck: typemoq.IMock<IDiagnosticsService>; + 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; setup(() => { + delete process.env.VSC_PYTHON_UNIT_TEST; + delete process.env.VSC_PYTHON_CI_TEST; serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); envHealthCheck = typemoq.Mock.ofType<IDiagnosticsService>(); - debuggerTypeCheck = typemoq.Mock.ofType<IDiagnosticsService>(); - outputChannel = typemoq.Mock.ofType<IOutputChannel>(); - logger = typemoq.Mock.ofType<ILogger>(); - - serviceContainer.setup(d => d.getAll(typemoq.It.isValue(IDiagnosticsService))) - .returns(() => [envHealthCheck.object, debuggerTypeCheck.object]); - serviceContainer.setup(d => d.get(typemoq.It.isValue(IOutputChannel), typemoq.It.isValue(STANDARD_OUTPUT_CHANNEL))) - .returns(() => outputChannel.object); - serviceContainer.setup(d => d.get(typemoq.It.isValue(ILogger))) - .returns(() => logger.object); - - appDiagnostics = new ApplicationDiagnostics(serviceContainer.object, outputChannel.object); + envHealthCheck.setup((service) => service.runInBackground).returns(() => true); + lsNotSupportedCheck = typemoq.Mock.ofType<IDiagnosticsService>(); + lsNotSupportedCheck.setup((service) => service.runInBackground).returns(() => false); + pythonInterpreterCheck = typemoq.Mock.ofType<IDiagnosticsService>(); + pythonInterpreterCheck.setup((service) => service.runInBackground).returns(() => false); + 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(IWorkspaceService))) + .returns(() => workspaceService.object); + + appDiagnostics = new ApplicationDiagnostics(serviceContainer.object); }); - test('Register should register source maps', () => { - const sourceMapService = typemoq.Mock.ofType<ISourceMapSupportService>(); - sourceMapService.setup(s => s.register()).verifiable(typemoq.Times.once()); + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + }); - 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 check Path environment variable and Debugger Type', async () => { - envHealthCheck.setup(e => e.diagnose()) + 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([])) + .verifiable(typemoq.Times.once()); + lsNotSupportedCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) .returns(() => Promise.resolve([])) .verifiable(typemoq.Times.once()); - debuggerTypeCheck.setup(e => e.diagnose()) + pythonInterpreterCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.never()); + + await appDiagnostics.performPreStartupHealthCheck(undefined); + + envHealthCheck.verifyAll(); + lsNotSupportedCheck.verifyAll(); + pythonInterpreterCheck.verifyAll(); + }); + + test('Performing Pre Startup Health Check must handles all validation checks only once either in background or foreground', async () => { + const diagnostic: IDiagnostic = { + code: 'Error' as any, + message: 'Error', + scope: undefined, + severity: undefined, + resource: undefined, + invokeHandler: 'default', + } as any; + envHealthCheck + .setup((e) => e.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([diagnostic])) + .verifiable(typemoq.Times.once()); + envHealthCheck + .setup((p) => p.handle(typemoq.It.isValue([diagnostic]))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + lsNotSupportedCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([diagnostic])) + .verifiable(typemoq.Times.once()); + lsNotSupportedCheck + .setup((p) => p.handle(typemoq.It.isValue([diagnostic]))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + pythonInterpreterCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([diagnostic])) + .verifiable(typemoq.Times.once()); + pythonInterpreterCheck + .setup((p) => p.handle(typemoq.It.isValue([diagnostic]))) + .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - await appDiagnostics.performPreStartupHealthCheck(); + await appDiagnostics.performPreStartupHealthCheck(undefined); + await sleep(1); + pythonInterpreterCheck.verifyAll(); + lsNotSupportedCheck.verifyAll(); envHealthCheck.verifyAll(); - debuggerTypeCheck.verifyAll(); }); - test('Diagnostics Returned by Per Startup Health Checks must be logged', async () => { + test('Diagnostics Returned by Pre Startup Health Checks must be logged', async () => { const diagnostics: IDiagnostic[] = []; - for (let i = 0; i <= (Math.random() * 10); i += 1) { + for (let i = 0; i <= Math.random() * 10; i += 1) { const diagnostic: IDiagnostic = { - code: `Error${i}`, + code: `Error${i}` as any, message: `Error${i}`, scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, - severity: DiagnosticSeverity.Error + severity: DiagnosticSeverity.Error, + resource: undefined, + invokeHandler: 'default', }; diagnostics.push(diagnostic); } - for (let i = 0; i <= (Math.random() * 10); i += 1) { + for (let i = 0; i <= Math.random() * 10; i += 1) { const diagnostic: IDiagnostic = { - code: `Warning${i}`, + code: `Warning${i}` as any, message: `Warning${i}`, scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, - severity: DiagnosticSeverity.Warning + severity: DiagnosticSeverity.Warning, + resource: undefined, + invokeHandler: 'default', }; diagnostics.push(diagnostic); } - for (let i = 0; i <= (Math.random() * 10); i += 1) { + for (let i = 0; i <= Math.random() * 10; i += 1) { const diagnostic: IDiagnostic = { - code: `Info${i}`, + code: `Info${i}` as any, message: `Info${i}`, scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, - severity: DiagnosticSeverity.Information + severity: DiagnosticSeverity.Information, + resource: undefined, + invokeHandler: 'default', }; diagnostics.push(diagnostic); } - for (const diagnostic of diagnostics) { - const message = `Diagnostic Code: ${diagnostic.code}, Message: ${diagnostic.message}`; - switch (diagnostic.severity) { - case DiagnosticSeverity.Error: { - logger.setup(l => l.logError(message)) - .verifiable(typemoq.Times.once()); - outputChannel.setup(o => o.appendLine(message)) - .verifiable(typemoq.Times.once()); - break; - } - case DiagnosticSeverity.Warning: { - logger.setup(l => l.logWarning(message)) - .verifiable(typemoq.Times.once()); - outputChannel.setup(o => o.appendLine(message)) - .verifiable(typemoq.Times.once()); - break; - } - default: { - logger.setup(l => l.logInformation(message)) - .verifiable(typemoq.Times.once()); - break; - } - } - } - - envHealthCheck.setup(e => e.diagnose()) + envHealthCheck + .setup((e) => e.diagnose(typemoq.It.isAny())) .returns(() => Promise.resolve(diagnostics)) .verifiable(typemoq.Times.once()); - debuggerTypeCheck.setup(e => e.diagnose()) + lsNotSupportedCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); + pythonInterpreterCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) .returns(() => Promise.resolve([])) .verifiable(typemoq.Times.once()); - await appDiagnostics.performPreStartupHealthCheck(); + await appDiagnostics.performPreStartupHealthCheck(undefined); + await sleep(1); envHealthCheck.verifyAll(); - debuggerTypeCheck.verifyAll(); - outputChannel.verifyAll(); - logger.verifyAll(); + lsNotSupportedCheck.verifyAll(); + pythonInterpreterCheck.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), + ]); + when(foreGroundService.runInBackground).thenReturn(false); + when(backGroundService.runInBackground).thenReturn(true); + + when(foreGroundService.diagnose(anything())).thenReturn(foreGroundDeferred.promise); + when(backGroundService.diagnose(anything())).thenReturn(backgroundGroundDeferred.promise); + + const service = new ApplicationDiagnostics(instance(svcContainer)); + + const promise = service.performPreStartupHealthCheck(undefined); + const deferred = createDeferredFromPromise(promise); + await sleep(1); + + verify(foreGroundService.runInBackground).atLeast(1); + verify(backGroundService.runInBackground).atLeast(1); + + assert.strictEqual(deferred.completed, false); + foreGroundDeferred.resolve([]); + await sleep(1); + + assert.strictEqual(deferred.completed, true); + + backgroundGroundDeferred.resolve([]); + await sleep(1); + verify(foreGroundService.diagnose(anything())).once(); + verify(backGroundService.diagnose(anything())).once(); }); }); diff --git a/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts b/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts index a2f678f1f142..c6c4ff06ee74 100644 --- a/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts +++ b/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts @@ -7,18 +7,28 @@ import { expect } from 'chai'; import * as path from 'path'; import * as typemoq from 'typemoq'; import { DiagnosticSeverity } from 'vscode'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; import { EnvironmentPathVariableDiagnosticsService } from '../../../../client/application/diagnostics/checks/envPathVariable'; import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; -import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; -import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticFilterService, IDiagnosticHandlerService, IDiagnosticsService } from '../../../../client/application/diagnostics/types'; -import { IApplicationEnvironment } from '../../../../client/common/application/types'; +import { + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt, +} from '../../../../client/application/diagnostics/promptHandler'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticFilterService, + IDiagnosticHandlerService, + IDiagnosticsService, +} from '../../../../client/application/diagnostics/types'; +import { IApplicationEnvironment, IWorkspaceService } from '../../../../client/common/application/types'; import { IPlatformService } from '../../../../client/common/platform/types'; import { ICurrentProcess, IPathUtils } from '../../../../client/common/types'; import { EnvironmentVariables } from '../../../../client/common/variables/types'; import { IServiceContainer } from '../../../../client/ioc/types'; -// tslint:disable-next-line:max-func-body-length suite('Application Diagnostics - Checks Env Path Variable', () => { let diagnosticService: IDiagnosticsService; let platformService: typemoq.IMock<IPlatformService>; @@ -33,44 +43,63 @@ suite('Application Diagnostics - Checks Env Path Variable', () => { setup(() => { const serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); platformService = typemoq.Mock.ofType<IPlatformService>(); - platformService.setup(p => p.pathVariableName).returns(() => pathVariableName); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IPlatformService))) + platformService.setup((p) => p.pathVariableName).returns(() => pathVariableName); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IPlatformService))) .returns(() => platformService.object); messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IDiagnosticHandlerService), typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId))) + serviceContainer + .setup((s) => + s.get( + typemoq.It.isValue(IDiagnosticHandlerService), + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId), + ), + ) .returns(() => messageHandler.object); appEnv = typemoq.Mock.ofType<IApplicationEnvironment>(); - appEnv.setup(a => a.extensionName).returns(() => extensionName); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IApplicationEnvironment))) - .returns(() => appEnv.object); + appEnv.setup((a) => a.extensionName).returns(() => extensionName); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IApplicationEnvironment))).returns(() => appEnv.object); filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) .returns(() => filterService.object); commandFactory = typemoq.Mock.ofType<IDiagnosticsCommandFactory>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) .returns(() => commandFactory.object); const currentProc = typemoq.Mock.ofType<ICurrentProcess>(); procEnv = typemoq.Mock.ofType<EnvironmentVariables>(); - currentProc.setup(p => p.env).returns(() => procEnv.object); - serviceContainer.setup(s => s.get(typemoq.It.isValue(ICurrentProcess))) - .returns(() => currentProc.object); + currentProc.setup((p) => p.env).returns(() => procEnv.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(ICurrentProcess))).returns(() => currentProc.object); const pathUtils = typemoq.Mock.ofType<IPathUtils>(); - pathUtils.setup(p => p.delimiter).returns(() => pathDelimiter); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IPathUtils))) - .returns(() => pathUtils.object); - - diagnosticService = new EnvironmentPathVariableDiagnosticsService(serviceContainer.object); + pathUtils.setup((p) => p.delimiter).returns(() => pathDelimiter); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); + const workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + workspaceService.setup((w) => w.getWorkspaceFolder(typemoq.It.isAny())).returns(() => undefined); + + diagnosticService = new (class extends EnvironmentPathVariableDiagnosticsService { + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + })(serviceContainer.object, []); + (diagnosticService as any)._clear(); }); test('Can handle EnvPathVariable diagnostics', async () => { const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) + diagnostic + .setup((d) => d.code) .returns(() => DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic) .verifiable(typemoq.Times.atLeastOnce()); @@ -80,8 +109,9 @@ suite('Application Diagnostics - Checks Env Path Variable', () => { }); test('Can not handle non-EnvPathVariable diagnostics', async () => { const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) - .returns(() => 'Something Else') + diagnostic + .setup((d) => d.code) + .returns(() => 'Something Else' as any) .verifiable(typemoq.Times.atLeastOnce()); const canHandle = await diagnosticService.canHandle(diagnostic.object); @@ -89,41 +119,35 @@ suite('Application Diagnostics - Checks Env Path Variable', () => { diagnostic.verifyAll(); }); test('Should return empty diagnostics for Mac', async () => { - platformService.setup(p => p.isMac).returns(() => true); - platformService.setup(p => p.isLinux).returns(() => false); - platformService.setup(p => p.isWindows).returns(() => false); - const diagnostics = await diagnosticService.diagnose(); + platformService.setup((p) => p.isMac).returns(() => true); + platformService.setup((p) => p.isLinux).returns(() => false); + platformService.setup((p) => p.isWindows).returns(() => false); + const diagnostics = await diagnosticService.diagnose(undefined); expect(diagnostics).to.be.deep.equal([]); }); test('Should return empty diagnostics for Linux', async () => { - platformService.setup(p => p.isMac).returns(() => false); - platformService.setup(p => p.isLinux).returns(() => true); - platformService.setup(p => p.isWindows).returns(() => false); - const diagnostics = await diagnosticService.diagnose(); + platformService.setup((p) => p.isMac).returns(() => false); + platformService.setup((p) => p.isLinux).returns(() => true); + platformService.setup((p) => p.isWindows).returns(() => false); + const diagnostics = await diagnosticService.diagnose(undefined); expect(diagnostics).to.be.deep.equal([]); }); test('Should return empty diagnostics for Windows if path variable is valid', async () => { - platformService.setup(p => p.isWindows).returns(() => true); - const paths = [ - path.join('one', 'two', 'three'), - path.join('one', 'two', 'four') - ].join(pathDelimiter); - procEnv.setup(env => env[pathVariableName]).returns(() => paths); + platformService.setup((p) => p.isWindows).returns(() => true); + const paths = [path.join('one', 'two', 'three'), path.join('one', 'two', 'four')].join(pathDelimiter); + procEnv.setup((env) => env[pathVariableName]).returns(() => paths); - const diagnostics = await diagnosticService.diagnose(); + const diagnostics = await diagnosticService.diagnose(undefined); expect(diagnostics).to.be.deep.equal([]); }); // Note: On windows, when a path contains a `;` then Windows encloses the path within `"`. - test('Should return single diagnostics for Windows if path contains \'"\'', async () => { - platformService.setup(p => p.isWindows).returns(() => true); - const paths = [ - path.join('one', 'two', 'three"'), - path.join('one', 'two', 'four') - ].join(pathDelimiter); - procEnv.setup(env => env[pathVariableName]).returns(() => paths); + test("Should return single diagnostics for Windows if path contains '\"'", async () => { + platformService.setup((p) => p.isWindows).returns(() => true); + const paths = [path.join('one', 'two', 'three"'), path.join('one', 'two', 'four')].join(pathDelimiter); + procEnv.setup((env) => env[pathVariableName]).returns(() => paths); - const diagnostics = await diagnosticService.diagnose(); + const diagnostics = await diagnosticService.diagnose(undefined); expect(diagnostics).to.be.lengthOf(1); expect(diagnostics[0].code).to.be.equal(DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic); @@ -133,35 +157,46 @@ suite('Application Diagnostics - Checks Env Path Variable', () => { expect(diagnostics[0].scope).to.be.equal(DiagnosticScope.Global); }); test('Should not return diagnostics for Windows if path ends with delimiter', async () => { - const paths = [ - path.join('one', 'two', 'three'), - path.join('one', 'two', 'four') - ].join(pathDelimiter) + pathDelimiter; - platformService.setup(p => p.isWindows).returns(() => true); - procEnv.setup(env => env[pathVariableName]).returns(() => paths); + const paths = + [path.join('one', 'two', 'three'), path.join('one', 'two', 'four')].join(pathDelimiter) + pathDelimiter; + platformService.setup((p) => p.isWindows).returns(() => true); + procEnv.setup((env) => env[pathVariableName]).returns(() => paths); - const diagnostics = await diagnosticService.diagnose(); + const diagnostics = await diagnosticService.diagnose(undefined); expect(diagnostics).to.be.lengthOf(0); }); test('Should display three options in message displayed with 2 commands', async () => { - platformService.setup(p => p.isWindows).returns(() => true); + platformService.setup((p) => p.isWindows).returns(() => true); const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) + diagnostic + .setup((d) => d.code) .returns(() => DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic) .verifiable(typemoq.Times.atLeastOnce()); const alwaysIgnoreCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); - commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ type: 'ignore', options: DiagnosticScope.Global }))) + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ + type: 'ignore', + options: DiagnosticScope.Global, + }), + ), + ) .returns(() => alwaysIgnoreCommand.object) .verifiable(typemoq.Times.once()); const launchBrowserCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); - commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }))) + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }), + ), + ) .returns(() => launchBrowserCommand.object) .verifiable(typemoq.Times.once()); - messageHandler.setup(m => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .verifiable(typemoq.Times.once()); + messageHandler.setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.once()); await diagnosticService.handle([diagnostic.object]); @@ -170,19 +205,23 @@ suite('Application Diagnostics - Checks Env Path Variable', () => { messageHandler.verifyAll(); }); test('Should not display a message if the diagnostic code has been ignored', async () => { - platformService.setup(p => p.isWindows).returns(() => true); + platformService.setup((p) => p.isWindows).returns(() => true); const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - filterService.setup(f => f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic))) + filterService + .setup((f) => + f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic)), + ) .returns(() => Promise.resolve(true)) .verifiable(typemoq.Times.once()); - diagnostic.setup(d => d.code) + diagnostic + .setup((d) => d.code) .returns(() => DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic) .verifiable(typemoq.Times.atLeastOnce()); - commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) - .verifiable(typemoq.Times.never()); - messageHandler.setup(m => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) + 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]); 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 736a85a0d0e9..000000000000 --- a/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts +++ /dev/null @@ -1,233 +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 - -import { expect } from 'chai'; -import * as path from 'path'; -import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; -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 { IWorkspaceService } from '../../../../client/common/application/types'; -import { IConfigurationService, IPythonSettings } from '../../../../client/common/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>; - 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>(); - 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 InvalidPythonPathInDebuggerService(serviceContainer.object, workspaceService.object, commandFactory.object, helper.object, configService.object); - }); - - test('Can handle InvalidPythonPathInDebugger diagnostics', async () => { - const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(true, 'Invalid value'); - diagnostic.verifyAll(); - }); - test('Can not handle non-InvalidPythonPathInDebugger diagnostics', async () => { - const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) - .returns(() => 'Something Else') - .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(); - expect(diagnostics).to.be.deep.equal([]); - }); - test('Should display one option to with a command', async () => { - const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) - .returns(() => DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - const interpreterSelectionCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); - commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', string>>({ 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('Ensure we get python path from config when path = ${config:python.pythonPath}', async () => { - const pythonPath = '${config:python.pythonPath}'; - - 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, 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 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 diagnosics are handled when path is invalid', async () => { - const pythonPath = path.join('a', 'b'); - let handleInvoked = false; - diagnosticService.handle = () => { 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); - - 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 179032c7acdc..000000000000 --- a/src/test/application/diagnostics/checks/lsNotSupported.unit.test.ts +++ /dev/null @@ -1,104 +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 { 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 { IServiceContainer } from '../../../../client/ioc/types'; - -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); - - diagnosticService = new LSNotSupportedDiagnosticService(serviceContainer.object, lsCompatibility.object, messageHandler.object); - }); - - 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') - .verifiable(TypeMoq.Times.atLeastOnce()); - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(false, 'Invalid value'); - diagnostic.verifyAll(); - }); -}); diff --git a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts new file mode 100644 index 000000000000..ba2436d0ffeb --- /dev/null +++ b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts @@ -0,0 +1,367 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { + InvalidMacPythonInterpreterDiagnostic, + InvalidMacPythonInterpreterService, +} from '../../../../client/application/diagnostics/checks/macPythonInterpreter'; +import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt, +} from '../../../../client/application/diagnostics/promptHandler'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticFilterService, + IDiagnosticHandlerService, + IDiagnosticsService, +} from '../../../../client/application/diagnostics/types'; +import { CommandsWithoutArgs } from '../../../../client/common/application/commands'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { + IConfigurationService, + IDisposableRegistry, + IInterpreterPathService, + InterpreterConfigurationScope, + IPythonSettings, +} from '../../../../client/common/types'; +import { sleep } from '../../../../client/common/utils/async'; +import { noop } from '../../../../client/common/utils/misc'; +import { IInterpreterHelper } from '../../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +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 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() { + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); + serviceContainer + .setup((s) => + s.get( + typemoq.It.isValue(IDiagnosticHandlerService), + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId), + ), + ) + .returns(() => messageHandler.object); + commandFactory = typemoq.Mock.ofType<IDiagnosticsCommandFactory>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + .returns(() => commandFactory.object); + settings = typemoq.Mock.ofType<IPythonSettings>(); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + const configService = typemoq.Mock.ofType<IConfigurationService>(); + configService.setup((c) => c.getSettings(typemoq.It.isAny())).returns(() => settings.object); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + platformService = typemoq.Mock.ofType<IPlatformService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IPlatformService))) + .returns(() => platformService.object); + helper = typemoq.Mock.ofType<IInterpreterHelper>(); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); + filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + + interpreterPathService = typemoq.Mock.ofType<IInterpreterPathService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IInterpreterPathService))) + .returns(() => interpreterPathService.object); + platformService + .setup((p) => p.isMac) + .returns(() => true) + .verifiable(typemoq.Times.once()); + return serviceContainer.object; + } + suite('Diagnostics', () => { + setup(() => { + diagnosticService = new (class extends InvalidMacPythonInterpreterService { + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + protected addPythonPathChangedHandler() { + noop(); + } + })(createContainer(), [], platformService.object, helper.object); + (diagnosticService as any)._clear(); + }); + + test('Can handle InvalidPythonPathInterpreter diagnostics', async () => { + for (const code of [DiagnosticCodes.MacInterpreterSelected]) { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => code) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(true, `Should be able to handle ${code}`); + diagnostic.verifyAll(); + } + }); + test('Can not handle non-InvalidPythonPathInterpreter diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => 'Something Else' as any) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); + test('Should return empty diagnostics if not a Mac', async () => { + platformService.reset(); + platformService + .setup((p) => p.isMac) + .returns(() => true) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([]); + platformService.verifyAll(); + }); + test('Should return empty diagnostics if 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(() => Promise.resolve(false)) + .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([]); + settings.verifyAll(); + platformService.verifyAll(); + helper.verifyAll(); + }); + 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(() => Promise.resolve(true)) + .verifiable(typemoq.Times.atLeastOnce()); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelected, undefined)], + 'not the same', + ); + }); + test('Handling no interpreters diagnostic should return select interpreter cmd', async () => { + const diagnostic = new InvalidMacPythonInterpreterDiagnostic( + DiagnosticCodes.MacInterpreterSelected, + undefined, + ); + const cmd = ({} as any) as IDiagnosticCommand; + const cmdIgnore = ({} as any) as IDiagnosticCommand; + let messagePrompt: MessageCommandPrompt | undefined; + messageHandler + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand', + }), + ), + ) + .returns(() => cmd) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ + type: 'ignore', + options: DiagnosticScope.Global, + }), + ), + ) + .returns(() => cmdIgnore) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); + expect(messagePrompt!.commandPrompts).to.be.deep.equal([ + { prompt: 'Select Python Interpreter', command: cmd }, + { prompt: "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.MacInterpreterSelected, + undefined, + ); + + filterService + .setup((f) => f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.MacInterpreterSelected))) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + commandFactory + .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + messageHandler + .setup((f) => f.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + filterService.verifyAll(); + commandFactory.verifyAll(); + }); + }); + + suite('Change Handlers.', () => { + test('Add PythonPath handler is invoked', async () => { + let invoked = false; + diagnosticService = new (class extends InvalidMacPythonInterpreterService { + protected addPythonPathChangedHandler() { + invoked = true; + } + })(createContainer(), [], platformService.object, helper.object); + + expect(invoked).to.be.equal(true, 'Not invoked'); + }); + 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.workspaceFolders) + .returns(() => [{ uri: '' }] as any) + .verifiable(typemoq.Times.once()); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + + const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { + constructor(arg1: IServiceContainer, arg3: IPlatformService, arg4: IInterpreterHelper) { + super(arg1, [], arg3, arg4); + this.changeThrottleTimeout = 1; + } + public onDidChangeConfigurationEx = (e: InterpreterConfigurationScope) => + super.onDidChangeConfiguration(e); + public diagnose(): Promise<any> { + diagnoseInvocationCount += 1; + return Promise.resolve(); + } + })( + serviceContainerObject, + typemoq.Mock.ofType<IPlatformService>().object, + typemoq.Mock.ofType<IInterpreterHelper>().object, + ); + + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + event.verifyAll(); + await sleep(100); + expect(diagnoseInvocationCount).to.be.equal(1, 'Not invoked'); + + await diagnosticSvc.onDidChangeConfigurationEx(event.object); + await sleep(100); + expect(diagnoseInvocationCount).to.be.equal(2, 'Not invoked'); + }); + + test('Diagnostics are checked and throttled 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.workspaceFolders) + .returns(() => [{ uri: '' }] as any) + .verifiable(typemoq.Times.once()); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + + const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { + constructor(arg1: IServiceContainer, arg3: IPlatformService, arg4: IInterpreterHelper) { + super(arg1, [], arg3, arg4); + this.changeThrottleTimeout = 100; + } + public onDidChangeConfigurationEx = (e: InterpreterConfigurationScope) => + super.onDidChangeConfiguration(e); + public diagnose(): Promise<any> { + diagnoseInvocationCount += 1; + return Promise.resolve(); + } + })( + serviceContainerObject, + typemoq.Mock.ofType<IPlatformService>().object, + typemoq.Mock.ofType<IInterpreterHelper>().object, + ); + + 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); + expect(diagnoseInvocationCount).to.be.equal(1, 'Not invoked'); + }); + + test('Ensure event Handler is registered correctly', async () => { + let interpreterPathServiceHandler: Function; + let invoked = false; + const workspaceService = { onDidChangeConfiguration: noop } as any; + const serviceContainerObject = createContainer(); + + 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(IWorkspaceService))).returns(() => workspaceService); + + 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 f0d63aa587b0..29a6c6eb3aff 100644 --- a/src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts +++ b/src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts @@ -5,18 +5,28 @@ import { expect } from 'chai'; import * as typemoq from 'typemoq'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; import { PowerShellActivationHackDiagnosticsService } from '../../../../client/application/diagnostics/checks/powerShellActivation'; import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; -import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; -import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticFilterService, IDiagnosticHandlerService, IDiagnosticsService } from '../../../../client/application/diagnostics/types'; -import { IApplicationEnvironment } from '../../../../client/common/application/types'; +import { + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt, +} from '../../../../client/application/diagnostics/promptHandler'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticFilterService, + IDiagnosticHandlerService, + IDiagnosticsService, +} from '../../../../client/application/diagnostics/types'; +import { IApplicationEnvironment, IWorkspaceService } from '../../../../client/common/application/types'; import { IPlatformService } from '../../../../client/common/platform/types'; import { ICurrentProcess, IPathUtils } from '../../../../client/common/types'; import { EnvironmentVariables } from '../../../../client/common/variables/types'; import { IServiceContainer } from '../../../../client/ioc/types'; -// tslint:disable-next-line:max-func-body-length suite('Application Diagnostics - PowerShell Activation', () => { let diagnosticService: IDiagnosticsService; let platformService: typemoq.IMock<IPlatformService>; @@ -31,44 +41,64 @@ suite('Application Diagnostics - PowerShell Activation', () => { setup(() => { const serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); platformService = typemoq.Mock.ofType<IPlatformService>(); - platformService.setup(p => p.pathVariableName).returns(() => pathVariableName); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IPlatformService))) + platformService.setup((p) => p.pathVariableName).returns(() => pathVariableName); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IPlatformService))) .returns(() => platformService.object); messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IDiagnosticHandlerService), typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId))) + serviceContainer + .setup((s) => + s.get( + typemoq.It.isValue(IDiagnosticHandlerService), + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId), + ), + ) .returns(() => messageHandler.object); appEnv = typemoq.Mock.ofType<IApplicationEnvironment>(); - appEnv.setup(a => a.extensionName).returns(() => extensionName); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IApplicationEnvironment))) - .returns(() => appEnv.object); + appEnv.setup((a) => a.extensionName).returns(() => extensionName); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IApplicationEnvironment))).returns(() => appEnv.object); filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) .returns(() => filterService.object); commandFactory = typemoq.Mock.ofType<IDiagnosticsCommandFactory>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) .returns(() => commandFactory.object); const currentProc = typemoq.Mock.ofType<ICurrentProcess>(); procEnv = typemoq.Mock.ofType<EnvironmentVariables>(); - currentProc.setup(p => p.env).returns(() => procEnv.object); - serviceContainer.setup(s => s.get(typemoq.It.isValue(ICurrentProcess))) - .returns(() => currentProc.object); + currentProc.setup((p) => p.env).returns(() => procEnv.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(ICurrentProcess))).returns(() => currentProc.object); const pathUtils = typemoq.Mock.ofType<IPathUtils>(); - pathUtils.setup(p => p.delimiter).returns(() => pathDelimiter); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IPathUtils))) - .returns(() => pathUtils.object); + pathUtils.setup((p) => p.delimiter).returns(() => pathDelimiter); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); - diagnosticService = new PowerShellActivationHackDiagnosticsService(serviceContainer.object); + const workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + workspaceService.setup((w) => w.getWorkspaceFolder(typemoq.It.isAny())).returns(() => undefined); + + diagnosticService = new (class extends PowerShellActivationHackDiagnosticsService { + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + })(serviceContainer.object, []); + (diagnosticService as any)._clear(); }); test('Can handle PowerShell diagnostics', async () => { const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) + diagnostic + .setup((d) => d.code) .returns(() => DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic) .verifiable(typemoq.Times.atLeastOnce()); @@ -78,8 +108,9 @@ suite('Application Diagnostics - PowerShell Activation', () => { }); test('Can not handle non-EnvPathVariable diagnostics', async () => { const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) - .returns(() => 'Something Else') + diagnostic + .setup((d) => d.code) + .returns(() => 'Something Else' as any) .verifiable(typemoq.Times.atLeastOnce()); const canHandle = await diagnosticService.canHandle(diagnostic.object); @@ -87,27 +118,42 @@ suite('Application Diagnostics - PowerShell Activation', () => { diagnostic.verifyAll(); }); test('Must return empty diagnostics', async () => { - const diagnostics = await diagnosticService.diagnose(); + const diagnostics = await diagnosticService.diagnose(undefined); expect(diagnostics).to.be.deep.equal([]); }); test('Should display three options in message displayed with 4 commands', async () => { const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); let options: MessageCommandPrompt | undefined; - diagnostic.setup(d => d.code) + diagnostic + .setup((d) => d.code) .returns(() => DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic) .verifiable(typemoq.Times.atLeastOnce()); const alwaysIgnoreCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); - commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ type: 'ignore', options: DiagnosticScope.Global }))) + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ + type: 'ignore', + options: DiagnosticScope.Global, + }), + ), + ) .returns(() => alwaysIgnoreCommand.object) .verifiable(typemoq.Times.once()); const launchBrowserCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); - commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }))) + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }), + ), + ) .returns(() => launchBrowserCommand.object) .verifiable(typemoq.Times.once()); - messageHandler.setup(m => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((_, opts: MessageCommandPrompt) => options = opts) + 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]); 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 d45e66cbb98e..2eecf052e433 100644 --- a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts @@ -3,77 +3,186 @@ 'use strict'; -// tslint:disable:max-func-body-length no-any max-classes-per-file - import { expect } from 'chai'; import * as typemoq from 'typemoq'; -import { ConfigurationChangeEvent } from 'vscode'; -import { InvalidPythonInterpreterDiagnostic, InvalidPythonInterpreterService } from '../../../../client/application/diagnostics/checks/pythonInterpreter'; +import { EventEmitter, Uri } from 'vscode'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { + DefaultShellDiagnostic, + InvalidPythonInterpreterDiagnostic, + InvalidPythonInterpreterService, +} from '../../../../client/application/diagnostics/checks/pythonInterpreter'; import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; -import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; -import { IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService, IDiagnosticsService } from '../../../../client/application/diagnostics/types'; -import { IWorkspaceService } from '../../../../client/common/application/types'; -import { IPlatformService } from '../../../../client/common/platform/types'; -import { IConfigurationService, IDisposableRegistry, IPythonSettings } from '../../../../client/common/types'; +import { + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt, +} from '../../../../client/application/diagnostics/promptHandler'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticHandlerService, +} from '../../../../client/application/diagnostics/types'; +import { CommandsWithoutArgs } from '../../../../client/common/application/commands'; +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, InterpreterType } from '../../../../client/interpreter/contracts'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../../client/ioc/types'; +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))) + serviceContainer + .setup((s) => + s.get( + typemoq.It.isValue(IDiagnosticHandlerService), + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId), + ), + ) .returns(() => messageHandler.object); commandFactory = typemoq.Mock.ofType<IDiagnosticsCommandFactory>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + 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))) + 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))) + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IPlatformService))) .returns(() => platformService.object); - helper = typemoq.Mock.ofType<IInterpreterHelper>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IInterpreterHelper))) - .returns(() => helper.object); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IDisposableRegistry))) - .returns(() => []); + 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; } suite('Diagnostics', () => { setup(() => { - diagnosticService = new class extends InvalidPythonInterpreterService { - protected addPythonPathChangedHandler() { noop(); } - }(createContainer()); + diagnosticService = new (class extends InvalidPythonInterpreterService { + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + protected addPythonPathChangedHandler() { + noop(); + } + })(createContainer(), []); + (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.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, - DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic, - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, ]) { const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) + diagnostic + .setup((d) => d.code) .returns(() => code) .verifiable(typemoq.Times.atLeastOnce()); @@ -82,183 +191,216 @@ 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') - .verifiable(typemoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(false, 'Invalid value'); - diagnostic.verifyAll(); - }); - test('Should return empty diagnostics if installer check is disabled', async () => { - settings - .setup(s => s.disableInstallationChecks) - .returns(() => true) - .verifiable(typemoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(); - expect(diagnostics).to.be.deep.equal([]); - settings.verifyAll(); + test('Should return empty diagnostics', async () => { + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([], 'not the same'); }); - test('Should return diagnostics if there are no interpreters', 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()); - - const diagnostics = await diagnosticService.diagnose(); - expect(diagnostics).to.be.deep.equal([new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.NoPythonInterpretersDiagnostic)]); - settings.verifyAll(); - interpreterService.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)) + .setup((i) => i.getInterpreters(undefined)) + .returns(() => []) .verifiable(typemoq.Times.once()); + + const diagnostics = await diagnosticService._manualDiagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [ + new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.NoPythonInterpretersDiagnostic, + undefined, + workspaceService.object, + DiagnosticScope.Global, + ), + ], + 'not the same', + ); + }); + 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.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()); + .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)); - const diagnostics = await diagnosticService.diagnose(); - expect(diagnostics).to.be.deep.equal([]); - settings.verifyAll(); - interpreterService.verifyAll(); - platformService.verifyAll(); + const diagnostics = await diagnosticService._manualDiagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [new DefaultShellDiagnostic(DiagnosticCodes.InvalidComspecDiagnostic, undefined)], + 'not the same', + ); }); - 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()); + 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(true)) - .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(typemoq.It.isAny())) - .returns(() => Promise.resolve([{} as any])) - .verifiable(typemoq.Times.never()); + .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({ 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.isAny())) - .returns(() => false) - .verifiable(typemoq.Times.once()); - - const diagnostics = await diagnosticService.diagnose(); - expect(diagnostics).to.be.deep.equal([]); - settings.verifyAll(); - interpreterService.verifyAll(); - platformService.verifyAll(); - helper.verifyAll(); + .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 })); + 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 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()); + test('Should return invalid interpreter diagnostics if there are interpreters but no current interpreter', async () => { interpreterService - .setup(i => i.getInterpreters(typemoq.It.isAny())) - .returns(() => Promise.resolve([ - { path: pythonPath } as any, - { path: pythonPath } as any - ])) + .setup((i) => i.hasInterpreters()) + .returns(() => Promise.resolve(true)) .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(); - expect(diagnostics).to.be.deep.equal([new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic)]); - settings.verifyAll(); - interpreterService.verifyAll(); - platformService.verifyAll(); - helper.verifyAll(); + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve(undefined); + }); + + 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 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()); + test('Should return empty diagnostics if there are interpreters and a current interpreter', async () => { + interpreterService.setup((i) => i.hasInterpreters()).returns(() => Promise.resolve(true)); interpreterService - .setup(i => i.getInterpreters(typemoq.It.isAny())) - .returns(() => Promise.resolve([ - { path: pythonPath } as any, - { path: pythonPath } as any, - { path: nonMacStandardInterpreter } as any - ])) - .verifiable(typemoq.Times.once()); - platformService - .setup(i => i.isMac) - .returns(() => true) + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => { + return Promise.resolve({ envType: EnvironmentType.Unknown } as any); + }); + + const diagnostics = await diagnosticService._manualDiagnose(undefined); + expect(diagnostics).to.be.deep.equal([], 'not the same'); + }); + + 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 + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) + .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - helper - .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) - .returns(() => true) - .verifiable(typemoq.Times.atLeastOnce()); - helper - .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(nonMacStandardInterpreter))) - .returns(() => false) - .verifiable(typemoq.Times.atLeastOnce()); - interpreterService - .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) - .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'launch', string>>({ + type: 'launch', + options: 'https://aka.ms/AAk3djo', + }), + ), + ) + .returns(() => cmd) .verifiable(typemoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(); - expect(diagnostics).to.be.deep.equal([new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic)]); - settings.verifyAll(); - interpreterService.verifyAll(); - platformService.verifyAll(); - helper.verifyAll(); + 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 no interpreters diagnostic should return download link', async () => { - const diagnostic = new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.NoPythonInterpretersDiagnostic); - const cmd = {} as any as IDiagnosticCommand; + + 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 - .setup(i => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) - .callback((d, p: MessageCommandPrompt) => messagePrompt = p) + .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' }))) + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'launch', string>>({ + type: 'launch', + options: 'https://aka.ms/AAk744c', + }), + ), + ) .returns(() => cmd) .verifiable(typemoq.Times.once()); @@ -267,21 +409,33 @@ 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!.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 - ); - const cmd = {} as any as IDiagnosticCommand; + + 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) + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', string>>({ type: 'executeVSCCommand' }))) + 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()); @@ -290,19 +444,37 @@ 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: 'Select Python Interpreter', command: cmd }]); + expect(messagePrompt!.commandPrompts).to.be.deep.equal([ + { + prompt: Common.seeInstructions, + command: cmd, + }, + ]); }); + test('Handling no interpreters diagnostic should return select interpreter cmd', async () => { - const diagnostic = new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic); - const cmd = {} as any as IDiagnosticCommand; + const diagnostic = new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.NoPythonInterpretersDiagnostic, + undefined, + workspaceService.object, + ); + 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) + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', string>>({ type: 'executeVSCCommand' }))) + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand', + options: Commands.Set_Interpreter, + }), + ), + ) .returns(() => cmd) .verifiable(typemoq.Times.once()); @@ -311,152 +483,110 @@ 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: 'Select Python Interpreter', command: cmd }]); + expect(messagePrompt!.commandPrompts).to.be.deep.equal([ + { + prompt: Common.selectPythonInterpreter, + command: cmd, + }, + ]); + expect(messagePrompt!.onClose).to.not.be.equal(undefined, 'onClose handler should be set.'); }); - test('Handling no interpreters diagnostisc should return download and learn links', async () => { - const diagnostic = new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic); - const cmdDownload = {} as any as IDiagnosticCommand; - const cmdLearn = {} 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.isValue(diagnostic), typemoq.It.isAny())) - .callback((d, p: MessageCommandPrompt) => messagePrompt = p) + .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<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand', + }), + ), + ) + .returns(() => cmd) + .verifiable(typemoq.Times.exactly(2)); 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 }]); + 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 empty diagnostic should not show a message nor return a command', async () => { + const diagnostics: IDiagnostic[] = []; + const cmd = ({} as any) as IDiagnosticCommand; - suite('Change Handlers.', () => { - test('Add PythonPath handler is invoked', async () => { - let invoked = false; - diagnosticService = new class extends InvalidPythonInterpreterService { - protected addPythonPathChangedHandler() { invoked = true; } - }(createContainer()); + messageHandler + .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand', + }), + ), + ) + .returns(() => cmd) + .verifiable(typemoq.Times.never()); - 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 => callbackHandler = cb } as any; - const serviceContainerObject = createContainer(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService); - diagnosticService = new class extends InvalidPythonInterpreterService { - protected async onDidChangeConfiguration(_event: ConfigurationChangeEvent) { invoked = true; } - }(serviceContainerObject); - - 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); - diagnosticService = new class extends InvalidPythonInterpreterService { - protected async onDidChangeConfiguration(_event: ConfigurationChangeEvent) { invoked = true; } - }(serviceContainerObject); - - expect(invoked).to.be.equal(false, 'Not invoked'); + await diagnosticService.handle(diagnostics); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); }); - test('Diagnostics are checked when path changes', async () => { - const event = typemoq.Mock.ofType<ConfigurationChangeEvent>(); - const workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); - const serviceContainerObject = createContainer(); - let diagnoseInvocationCount = 0; - workspaceService - .setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(typemoq.Times.once()); - workspaceService - .setup(w => w.workspaceFolders) - .returns(() => [{ uri: '' }] as any) - .verifiable(typemoq.Times.once()); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - const diagnosticSvc = new class extends InvalidPythonInterpreterService { - constructor(item) { - super(item); - this.changeThrottleTimeout = 1; - } - public onDidChangeConfigurationEx = e => super.onDidChangeConfiguration(e); - public diagnose(): Promise<any> { - diagnoseInvocationCount += 1; - return Promise.resolve(); - } - }(serviceContainerObject); + 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 + >; - event - .setup(e => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) - .returns(() => true) - .verifiable(typemoq.Times.atLeastOnce()); + diagnosticServiceMock.setup((f) => f.canHandle(typemoq.It.isAny())).returns(() => Promise.resolve(false)); + messageHandler + .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand', + }), + ), + ) + .returns(() => cmd) + .verifiable(typemoq.Times.never()); - await diagnosticSvc.onDidChangeConfigurationEx(event.object); - event.verifyAll(); - await sleep(100); - expect(diagnoseInvocationCount).to.be.equal(1, 'Not invoked'); + await diagnosticServiceMock.object.handle([diagnostic]); - await diagnosticSvc.onDidChangeConfigurationEx(event.object); - await sleep(100); - expect(diagnoseInvocationCount).to.be.equal(2, 'Not invoked'); - }); - test('Diagnostics are checked and throttled when path changes', async () => { - const event = typemoq.Mock.ofType<ConfigurationChangeEvent>(); - const workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); - const serviceContainerObject = createContainer(); - let diagnoseInvocationCount = 0; - workspaceService - .setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(typemoq.Times.once()); - workspaceService - .setup(w => w.workspaceFolders) - .returns(() => [{ uri: '' }] as any) - .verifiable(typemoq.Times.once()); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - const diagnosticSvc = new class extends InvalidPythonInterpreterService { - constructor(item) { - super(item); - this.changeThrottleTimeout = 100; - } - public onDidChangeConfigurationEx = e => super.onDidChangeConfiguration(e); - public diagnose(): Promise<any> { - diagnoseInvocationCount += 1; - return Promise.resolve(); - } - }(serviceContainerObject); - - 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'); + messageHandler.verifyAll(); + commandFactory.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/commands/execVSCCommands.unit.test.ts b/src/test/application/diagnostics/commands/execVSCCommands.unit.test.ts new file mode 100644 index 000000000000..24881c71833b --- /dev/null +++ b/src/test/application/diagnostics/commands/execVSCCommands.unit.test.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { ExecuteVSCCommand } from '../../../../client/application/diagnostics/commands/execVSCCommand'; +import { DiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/factory'; +import { IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { IDiagnostic } from '../../../../client/application/diagnostics/types'; +import { ICommandManager } from '../../../../client/common/application/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Exec VSC Commands', () => { + let commandFactory: IDiagnosticsCommandFactory; + let commandManager: typemoq.IMock<ICommandManager>; + setup(() => { + const serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + commandManager = typemoq.Mock.ofType<ICommandManager>(); + serviceContainer + .setup((svc) => svc.get<ICommandManager>(typemoq.It.isValue(ICommandManager), typemoq.It.isAny())) + .returns(() => commandManager.object); + commandFactory = new DiagnosticsCommandFactory(serviceContainer.object); + }); + + test('Test creation of VSC Command', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + + const command = commandFactory.createCommand(diagnostic.object, { + type: 'executeVSCCommand', + options: 'editor.action.formatDocument', + }); + expect(command).to.be.instanceOf(ExecuteVSCCommand); + }); + + test('Test execution of VSC Command', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + commandManager + .setup((cmd) => cmd.executeCommand('editor.action.formatDocument')) + .returns(() => Promise.resolve(undefined)) + .verifiable(typemoq.Times.once()); + + const command = commandFactory.createCommand(diagnostic.object, { + type: 'executeVSCCommand', + options: 'editor.action.formatDocument', + }); + await command.invoke(); + + expect(command).to.be.instanceOf(ExecuteVSCCommand); + commandManager.verifyAll(); + }); +}); diff --git a/src/test/application/diagnostics/commands/factory.unit.test.ts b/src/test/application/diagnostics/commands/factory.unit.test.ts index 187499f6d652..82db96aa3dec 100644 --- a/src/test/application/diagnostics/commands/factory.unit.test.ts +++ b/src/test/application/diagnostics/commands/factory.unit.test.ts @@ -22,7 +22,10 @@ suite('Application Diagnostics - Commands Factory', () => { test('Test creation of Ignore Command', async () => { const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - const command = commandFactory.createCommand(diagnostic.object, { type: 'ignore', options: DiagnosticScope.Global }); + const command = commandFactory.createCommand(diagnostic.object, { + type: 'ignore', + 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 586071b9abe0..90c8e38f8470 100644 --- a/src/test/application/diagnostics/commands/ignore.unit.test.ts +++ b/src/test/application/diagnostics/commands/ignore.unit.test.ts @@ -5,7 +5,12 @@ import * as typemoq from 'typemoq'; import { IgnoreDiagnosticCommand } from '../../../../client/application/diagnostics/commands/ignore'; -import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticFilterService } from '../../../../client/application/diagnostics/types'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticFilterService, +} from '../../../../client/application/diagnostics/types'; import { IServiceContainer } from '../../../../client/ioc/types'; suite('Application Diagnostics - Commands Ignore', () => { @@ -21,12 +26,16 @@ suite('Application Diagnostics - Commands Ignore', () => { test('Invoking Command should invoke the filter Service', async () => { const filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) .returns(() => filterService.object) .verifiable(typemoq.Times.once()); - diagnostic.setup(d => d.code).returns(() => 'xyz') + diagnostic + .setup((d) => d.code) + .returns(() => 'xyz' as any) .verifiable(typemoq.Times.once()); - filterService.setup(s => s.ignoreDiagnostic(typemoq.It.isValue('xyz'), typemoq.It.isValue(DiagnosticScope.Global))) + filterService + .setup((s) => s.ignoreDiagnostic(typemoq.It.isValue('xyz'), typemoq.It.isValue(DiagnosticScope.Global))) .verifiable(typemoq.Times.once()); await ignoreCommand.invoke(); diff --git a/src/test/application/diagnostics/commands/launchBrowser.unit.test.ts b/src/test/application/diagnostics/commands/launchBrowser.unit.test.ts index 665f7937934a..5b85621971fc 100644 --- a/src/test/application/diagnostics/commands/launchBrowser.unit.test.ts +++ b/src/test/application/diagnostics/commands/launchBrowser.unit.test.ts @@ -22,11 +22,11 @@ suite('Application Diagnostics - Commands Launch Browser', () => { test('Invoking Command should launch the browser', async () => { const browser = typemoq.Mock.ofType<IBrowserService>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IBrowserService))) + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IBrowserService))) .returns(() => browser.object) .verifiable(typemoq.Times.once()); - browser.setup(s => s.launch(typemoq.It.isValue(url))) - .verifiable(typemoq.Times.once()); + browser.setup((s) => s.launch(typemoq.It.isValue(url))).verifiable(typemoq.Times.once()); await cmd.invoke(); serviceContainer.verifyAll(); diff --git a/src/test/application/diagnostics/filter.unit.test.ts b/src/test/application/diagnostics/filter.unit.test.ts index b42136fc6d8d..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'; @@ -18,102 +16,141 @@ suite('Application Diagnostics - Filter', () => { [ { name: 'Global', scope: DiagnosticScope.Global, state: () => globalState, otherState: () => workspaceState }, - { name: 'Workspace', scope: DiagnosticScope.WorkspaceFolder, state: () => workspaceState, otherState: () => globalState } - ] - .forEach(item => { - let serviceContainer: typemoq.IMock<IServiceContainer>; - let filterService: IDiagnosticFilterService; - - setup(() => { - globalState = typemoq.Mock.ofType<IPersistentState<string[]>>(); - workspaceState = typemoq.Mock.ofType<IPersistentState<string[]>>(); - - serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); - const stateFactory = typemoq.Mock.ofType<IPersistentStateFactory>(); - - stateFactory.setup(f => f.createGlobalPersistentState<string[]>(typemoq.It.isValue(FilterKeys.GlobalDiagnosticFilter), typemoq.It.isValue([]))) - .returns(() => globalState.object); - stateFactory.setup(f => f.createWorkspacePersistentState<string[]>(typemoq.It.isValue(FilterKeys.WorkspaceDiagnosticFilter), typemoq.It.isValue([]))) - .returns(() => workspaceState.object); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IPersistentStateFactory))) - .returns(() => stateFactory.object); - - filterService = new DiagnosticFilterService(serviceContainer.object); - }); - - test(`ignoreDiagnostic must save codes in ${item.name} Persistent State`, async () => { - const code = 'xyz'; - item.state().setup(g => g.value).returns(() => []) - .verifiable(typemoq.Times.once()); - item.state().setup(g => g.updateValue(typemoq.It.isValue([code]))) - .verifiable(typemoq.Times.once()); - - item.otherState().setup(g => g.value) - .verifiable(typemoq.Times.never()); - item.otherState().setup(g => g.updateValue(typemoq.It.isAny())) - .verifiable(typemoq.Times.never()); - - await filterService.ignoreDiagnostic(code, item.scope); - - item.state().verifyAll(); - }); - test('shouldIgnoreDiagnostic should return \'false\' when code does not exist in any State', async () => { - const code = 'xyz'; - item.state().setup(g => g.value).returns(() => []) - .verifiable(typemoq.Times.once()); - item.otherState().setup(g => g.value).returns(() => []) - .verifiable(typemoq.Times.once()); - - const ignore = await filterService.shouldIgnoreDiagnostic(code); - - expect(ignore).to.be.equal(false, 'Incorrect value'); - item.state().verifyAll(); - }); - test(`shouldIgnoreDiagnostic should return \'true\' when code exist in ${item.name} State`, async () => { - const code = 'xyz'; - item.state().setup(g => g.value).returns(() => ['a', 'b', 'c', code]) - .verifiable(typemoq.Times.once()); - item.otherState().setup(g => g.value).returns(() => []) - .verifiable(typemoq.Times.once()); - - const ignore = await filterService.shouldIgnoreDiagnostic(code); - - expect(ignore).to.be.equal(true, 'Incorrect value'); - item.state().verifyAll(); - }); - - test('shouldIgnoreDiagnostic should return \'true\' when code exist in any State', async () => { - const code = 'xyz'; - item.state().setup(g => g.value).returns(() => []) - .verifiable(typemoq.Times.atLeast(0)); - item.otherState().setup(g => g.value).returns(() => ['a', 'b', 'c', code]) - .verifiable(typemoq.Times.atLeast(0)); - - const ignore = await filterService.shouldIgnoreDiagnostic(code); - - expect(ignore).to.be.equal(true, 'Incorrect value'); - item.state().verifyAll(); - }); - - test(`ignoreDiagnostic must append codes in ${item.name} Persistent State`, async () => { - const code = 'xyz'; - const currentState = ['a', 'b', 'c']; - item.state().setup(g => g.value).returns(() => currentState) - .verifiable(typemoq.Times.atLeastOnce()); - item.state().setup(g => g.updateValue(typemoq.It.isAny())) - .callback(value => { - expect(value).to.deep.equal(currentState.concat([code])); - }) - .verifiable(typemoq.Times.atLeastOnce()); - - item.otherState().setup(g => g.value) - .verifiable(typemoq.Times.never()); - item.otherState().setup(g => g.updateValue(typemoq.It.isAny())) - .verifiable(typemoq.Times.never()); - - await filterService.ignoreDiagnostic(code, item.scope); - - item.state().verifyAll(); - }); + { + name: 'Workspace', + scope: DiagnosticScope.WorkspaceFolder, + state: () => workspaceState, + otherState: () => globalState, + }, + ].forEach((item) => { + let serviceContainer: typemoq.IMock<IServiceContainer>; + let filterService: IDiagnosticFilterService; + + setup(() => { + globalState = typemoq.Mock.ofType<IPersistentState<string[]>>(); + workspaceState = typemoq.Mock.ofType<IPersistentState<string[]>>(); + + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + const stateFactory = typemoq.Mock.ofType<IPersistentStateFactory>(); + + stateFactory + .setup((f) => + f.createGlobalPersistentState<string[]>( + typemoq.It.isValue(FilterKeys.GlobalDiagnosticFilter), + typemoq.It.isValue([]), + ), + ) + .returns(() => globalState.object); + stateFactory + .setup((f) => + f.createWorkspacePersistentState<string[]>( + typemoq.It.isValue(FilterKeys.WorkspaceDiagnosticFilter), + typemoq.It.isValue([]), + ), + ) + .returns(() => workspaceState.object); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IPersistentStateFactory))) + .returns(() => stateFactory.object); + + filterService = new DiagnosticFilterService(serviceContainer.object); + }); + + test(`ignoreDiagnostic must save codes in ${item.name} Persistent State`, async () => { + const code = 'xyz'; + item.state() + .setup((g) => g.value) + .returns(() => []) + .verifiable(typemoq.Times.once()); + item.state() + .setup((g) => g.updateValue(typemoq.It.isValue([code]))) + .verifiable(typemoq.Times.once()); + + item.otherState() + .setup((g) => g.value) + .verifiable(typemoq.Times.never()); + item.otherState() + .setup((g) => g.updateValue(typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await filterService.ignoreDiagnostic(code, item.scope); + + item.state().verifyAll(); + }); + test("shouldIgnoreDiagnostic should return 'false' when code does not exist in any State", async () => { + const code = 'xyz'; + item.state() + .setup((g) => g.value) + .returns(() => []) + .verifiable(typemoq.Times.once()); + item.otherState() + .setup((g) => g.value) + .returns(() => []) + .verifiable(typemoq.Times.once()); + + const ignore = await filterService.shouldIgnoreDiagnostic(code); + + expect(ignore).to.be.equal(false, 'Incorrect value'); + item.state().verifyAll(); + }); + test(`shouldIgnoreDiagnostic should return \'true\' when code exist in ${item.name} State`, async () => { + const code = 'xyz'; + item.state() + .setup((g) => g.value) + .returns(() => ['a', 'b', 'c', code]) + .verifiable(typemoq.Times.once()); + item.otherState() + .setup((g) => g.value) + .returns(() => []) + .verifiable(typemoq.Times.once()); + + const ignore = await filterService.shouldIgnoreDiagnostic(code); + + expect(ignore).to.be.equal(true, 'Incorrect value'); + item.state().verifyAll(); + }); + + test("shouldIgnoreDiagnostic should return 'true' when code exist in any State", async () => { + const code = 'xyz'; + item.state() + .setup((g) => g.value) + .returns(() => []) + .verifiable(typemoq.Times.atLeast(0)); + item.otherState() + .setup((g) => g.value) + .returns(() => ['a', 'b', 'c', code]) + .verifiable(typemoq.Times.atLeast(0)); + + const ignore = await filterService.shouldIgnoreDiagnostic(code); + + expect(ignore).to.be.equal(true, 'Incorrect value'); + item.state().verifyAll(); + }); + + test(`ignoreDiagnostic must append codes in ${item.name} Persistent State`, async () => { + const code = 'xyz'; + const currentState = ['a', 'b', 'c']; + item.state() + .setup((g) => g.value) + .returns(() => currentState) + .verifiable(typemoq.Times.atLeastOnce()); + item.state() + .setup((g) => g.updateValue(typemoq.It.isAny())) + .callback((value) => { + expect(value).to.deep.equal(currentState.concat([code])); + }) + .verifiable(typemoq.Times.atLeastOnce()); + + item.otherState() + .setup((g) => g.value) + .verifiable(typemoq.Times.never()); + item.otherState() + .setup((g) => g.updateValue(typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await filterService.ignoreDiagnostic(code, item.scope); + + item.state().verifyAll(); }); + }); }); diff --git a/src/test/application/diagnostics/promptHandler.unit.test.ts b/src/test/application/diagnostics/promptHandler.unit.test.ts index 4867af1e51f8..0c8d732b15f4 100644 --- a/src/test/application/diagnostics/promptHandler.unit.test.ts +++ b/src/test/application/diagnostics/promptHandler.unit.test.ts @@ -3,12 +3,19 @@ 'use strict'; -// tslint:disable:insecure-random max-func-body-length - +import { expect } from 'chai'; import * as typemoq from 'typemoq'; import { DiagnosticSeverity } from 'vscode'; -import { DiagnosticCommandPromptHandlerService, MessageCommandPrompt } from '../../../client/application/diagnostics/promptHandler'; -import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService } from '../../../client/application/diagnostics/types'; +import { + DiagnosticCommandPromptHandlerService, + MessageCommandPrompt, +} from '../../../client/application/diagnostics/promptHandler'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticHandlerService, +} from '../../../client/application/diagnostics/types'; import { IApplicationShell } from '../../../client/common/application/types'; import { getNamesAndValues } from '../../../client/common/utils/enum'; import { IServiceContainer } from '../../../client/ioc/types'; @@ -22,28 +29,37 @@ suite('Application Diagnostics - PromptHandler', () => { serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); appShell = typemoq.Mock.ofType<IApplicationShell>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IApplicationShell))) - .returns(() => appShell.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IApplicationShell))).returns(() => appShell.object); promptHandler = new DiagnosticCommandPromptHandlerService(serviceContainer.object); }); - getNamesAndValues<DiagnosticSeverity>(DiagnosticSeverity).forEach(severity => { + getNamesAndValues<DiagnosticSeverity>(DiagnosticSeverity).forEach((severity) => { test(`Handling a diagnositic of severity '${severity.name}' should display a message without any buttons`, async () => { - const diagnostic: IDiagnostic = { code: '1', message: 'one', scope: DiagnosticScope.Global, severity: severity.value }; + const diagnostic: IDiagnostic = { + code: '1' as any, + message: 'one', + scope: DiagnosticScope.Global, + severity: severity.value, + resource: undefined, + invokeHandler: 'default', + }; switch (severity.value) { case DiagnosticSeverity.Error: { - appShell.setup(a => a.showErrorMessage(typemoq.It.isValue(diagnostic.message))) + appShell + .setup((a) => a.showErrorMessage(typemoq.It.isValue(diagnostic.message))) .verifiable(typemoq.Times.once()); break; } case DiagnosticSeverity.Warning: { - appShell.setup(a => a.showWarningMessage(typemoq.It.isValue(diagnostic.message))) + appShell + .setup((a) => a.showWarningMessage(typemoq.It.isValue(diagnostic.message))) .verifiable(typemoq.Times.once()); break; } default: { - appShell.setup(a => a.showInformationMessage(typemoq.It.isValue(diagnostic.message))) + appShell + .setup((a) => a.showInformationMessage(typemoq.It.isValue(diagnostic.message))) .verifiable(typemoq.Times.once()); break; } @@ -52,32 +68,118 @@ suite('Application Diagnostics - PromptHandler', () => { await promptHandler.handle(diagnostic); appShell.verifyAll(); }); + test(`Handling a diagnositic of severity '${severity.name}' should invoke the onClose handler`, async () => { + const diagnostic: IDiagnostic = { + code: '1' as any, + message: 'one', + scope: DiagnosticScope.Global, + severity: severity.value, + resource: undefined, + invokeHandler: 'default', + }; + let onCloseHandlerInvoked = false; + const options: MessageCommandPrompt = { + commandPrompts: [{ prompt: 'Yes' }, { prompt: 'No' }], + message: 'Custom Message', + onClose: () => { + onCloseHandlerInvoked = true; + }, + }; + + switch (severity.value) { + case DiagnosticSeverity.Error: { + appShell + .setup((a) => + a.showErrorMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No'), + ), + ) + .returns(() => Promise.resolve('Yes')) + .verifiable(typemoq.Times.once()); + break; + } + case DiagnosticSeverity.Warning: { + appShell + .setup((a) => + a.showWarningMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No'), + ), + ) + .returns(() => Promise.resolve('Yes')) + .verifiable(typemoq.Times.once()); + break; + } + default: { + appShell + .setup((a) => + a.showInformationMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No'), + ), + ) + .returns(() => Promise.resolve('Yes')) + .verifiable(typemoq.Times.once()); + break; + } + } + + await promptHandler.handle(diagnostic, options); + appShell.verifyAll(); + expect(onCloseHandlerInvoked).to.equal(true, 'onClose handler should be called.'); + }); test(`Handling a diagnositic of severity '${severity.name}' should display a custom message with buttons`, async () => { - const diagnostic: IDiagnostic = { code: '1', message: 'one', scope: DiagnosticScope.Global, severity: severity.value }; + const diagnostic: IDiagnostic = { + code: '1' as any, + message: 'one', + scope: DiagnosticScope.Global, + severity: severity.value, + resource: undefined, + invokeHandler: 'default', + }; const options: MessageCommandPrompt = { - commandPrompts: [ - { prompt: 'Yes' }, - { prompt: 'No' } - ], - message: 'Custom Message' + commandPrompts: [{ prompt: 'Yes' }, { prompt: 'No' }], + message: 'Custom Message', }; switch (severity.value) { case DiagnosticSeverity.Error: { - appShell.setup(a => a.showErrorMessage(typemoq.It.isValue(options.message!), - typemoq.It.isValue('Yes'), typemoq.It.isValue('No'))) + appShell + .setup((a) => + a.showErrorMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No'), + ), + ) .verifiable(typemoq.Times.once()); break; } case DiagnosticSeverity.Warning: { - appShell.setup(a => a.showWarningMessage(typemoq.It.isValue(options.message!), - typemoq.It.isValue('Yes'), typemoq.It.isValue('No'))) + appShell + .setup((a) => + a.showWarningMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No'), + ), + ) .verifiable(typemoq.Times.once()); break; } default: { - appShell.setup(a => a.showInformationMessage(typemoq.It.isValue(options.message!), - typemoq.It.isValue('Yes'), typemoq.It.isValue('No'))) + appShell + .setup((a) => + a.showInformationMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No'), + ), + ) .verifiable(typemoq.Times.once()); break; } @@ -87,36 +189,60 @@ suite('Application Diagnostics - PromptHandler', () => { appShell.verifyAll(); }); test(`Handling a diagnositic of severity '${severity.name}' should display a custom message with buttons and invoke selected command`, async () => { - const diagnostic: IDiagnostic = { code: '1', message: 'one', scope: DiagnosticScope.Global, severity: severity.value }; + const diagnostic: IDiagnostic = { + code: '1' as any, + message: 'one', + scope: DiagnosticScope.Global, + severity: severity.value, + resource: undefined, + invokeHandler: 'default', + }; const command = typemoq.Mock.ofType<IDiagnosticCommand>(); const options: MessageCommandPrompt = { commandPrompts: [ { prompt: 'Yes', command: command.object }, - { prompt: 'No', command: command.object } + { prompt: 'No', command: command.object }, ], - message: 'Custom Message' + message: 'Custom Message', }; - command.setup(c => c.invoke()) - .verifiable(typemoq.Times.once()); + command.setup((c) => c.invoke()).verifiable(typemoq.Times.once()); switch (severity.value) { case DiagnosticSeverity.Error: { - appShell.setup(a => a.showErrorMessage(typemoq.It.isValue(options.message!), - typemoq.It.isValue('Yes'), typemoq.It.isValue('No'))) + appShell + .setup((a) => + a.showErrorMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No'), + ), + ) .returns(() => Promise.resolve('Yes')) .verifiable(typemoq.Times.once()); break; } case DiagnosticSeverity.Warning: { - appShell.setup(a => a.showWarningMessage(typemoq.It.isValue(options.message!), - typemoq.It.isValue('Yes'), typemoq.It.isValue('No'))) + appShell + .setup((a) => + a.showWarningMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No'), + ), + ) .returns(() => Promise.resolve('Yes')) .verifiable(typemoq.Times.once()); break; } default: { - appShell.setup(a => a.showInformationMessage(typemoq.It.isValue(options.message!), - typemoq.It.isValue('Yes'), typemoq.It.isValue('No'))) + appShell + .setup((a) => + a.showInformationMessage( + typemoq.It.isValue(options.message!), + typemoq.It.isValue('Yes'), + typemoq.It.isValue('No'), + ), + ) .returns(() => Promise.resolve('Yes')) .verifiable(typemoq.Times.once()); break; diff --git a/src/test/application/diagnostics/serviceRegistry.unit.test.ts b/src/test/application/diagnostics/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..dcff47b2b7e7 --- /dev/null +++ b/src/test/application/diagnostics/serviceRegistry.unit.test.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { IExtensionSingleActivationService } from '../../../client/activation/types'; +import { ApplicationDiagnostics } from '../../../client/application/diagnostics/applicationDiagnostics'; +import { + EnvironmentPathVariableDiagnosticsService, + EnvironmentPathVariableDiagnosticsServiceId, +} from '../../../client/application/diagnostics/checks/envPathVariable'; +import { + InvalidLaunchJsonDebuggerService, + InvalidLaunchJsonDebuggerServiceId, +} from '../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; +import { + JediPython27NotSupportedDiagnosticService, + JediPython27NotSupportedDiagnosticServiceId, +} from '../../../client/application/diagnostics/checks/jediPython27NotSupported'; +import { + InvalidMacPythonInterpreterService, + InvalidMacPythonInterpreterServiceId, +} from '../../../client/application/diagnostics/checks/macPythonInterpreter'; +import { + PowerShellActivationHackDiagnosticsService, + PowerShellActivationHackDiagnosticsServiceId, +} from '../../../client/application/diagnostics/checks/powerShellActivation'; +import { + InvalidPythonInterpreterService, + InvalidPythonInterpreterServiceId, +} from '../../../client/application/diagnostics/checks/pythonInterpreter'; +import { + 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, +} from '../../../client/application/diagnostics/promptHandler'; +import { registerTypes } from '../../../client/application/diagnostics/serviceRegistry'; +import { + IDiagnosticFilterService, + IDiagnosticHandlerService, + IDiagnosticsService, +} from '../../../client/application/diagnostics/types'; +import { IApplicationDiagnostics } from '../../../client/application/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; + +suite('Application Diagnostics - Register classes in IOC Container', () => { + let serviceManager: IServiceManager; + setup(() => { + serviceManager = mock(ServiceManager); + }); + test('Register Classes', () => { + registerTypes(instance(serviceManager)); + + verify( + serviceManager.addSingleton<IDiagnosticFilterService>(IDiagnosticFilterService, DiagnosticFilterService), + ); + verify( + serviceManager.addSingleton<IDiagnosticHandlerService<MessageCommandPrompt>>( + IDiagnosticHandlerService, + DiagnosticCommandPromptHandlerService, + DiagnosticCommandPromptHandlerServiceId, + ), + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + EnvironmentPathVariableDiagnosticsService, + EnvironmentPathVariableDiagnosticsServiceId, + ), + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + InvalidLaunchJsonDebuggerService, + InvalidLaunchJsonDebuggerServiceId, + ), + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + InvalidPythonInterpreterService, + InvalidPythonInterpreterServiceId, + ), + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + InvalidPythonInterpreterService, + InvalidPythonInterpreterServiceId, + ), + ); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + InvalidPythonInterpreterService, + ), + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + JediPython27NotSupportedDiagnosticService, + JediPython27NotSupportedDiagnosticServiceId, + ), + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + PowerShellActivationHackDiagnosticsService, + PowerShellActivationHackDiagnosticsServiceId, + ), + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + InvalidMacPythonInterpreterService, + InvalidMacPythonInterpreterServiceId, + ), + ); + verify( + serviceManager.addSingleton<IDiagnosticsService>( + IDiagnosticsService, + SwitchToDefaultLanguageServerDiagnosticService, + SwitchToDefaultLanguageServerDiagnosticServiceId, + ), + ); + verify( + serviceManager.addSingleton<IDiagnosticsCommandFactory>( + IDiagnosticsCommandFactory, + 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 d1cda03650e6..000000000000 --- a/src/test/application/diagnostics/sourceMapSupportService.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: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/ciConstants.ts b/src/test/ciConstants.ts index 1be1ccf6a339..7bc24e3d2afa 100644 --- a/src/test/ciConstants.ts +++ b/src/test/ciConstants.ts @@ -10,7 +10,8 @@ export const PYTHON_VIRTUAL_ENVS_LOCATION = process.env.PYTHON_VIRTUAL_ENVS_LOCA export const IS_APPVEYOR = process.env.APPVEYOR === 'true'; export const IS_TRAVIS = process.env.TRAVIS === 'true'; export const IS_VSTS = process.env.TF_BUILD !== undefined; -export const IS_CI_SERVER = IS_TRAVIS || IS_APPVEYOR || IS_VSTS; +export const IS_GITHUB_ACTIONS = process.env.GITHUB_ACTIONS === 'true'; +export const IS_CI_SERVER = IS_TRAVIS || IS_APPVEYOR || IS_VSTS || IS_GITHUB_ACTIONS; // Control JUnit-style output logging for reporting purposes. let reportJunit: boolean = false; @@ -18,10 +19,4 @@ if (IS_CI_SERVER && process.env.MOCHA_REPORTER_JUNIT !== undefined) { reportJunit = process.env.MOCHA_REPORTER_JUNIT.toLowerCase() === 'true'; } export const MOCHA_REPORTER_JUNIT: boolean = reportJunit; -export const MOCHA_CI_REPORTFILE: string = MOCHA_REPORTER_JUNIT && process.env.MOCHA_CI_REPORTFILE !== undefined ? - process.env.MOCHA_CI_REPORTFILE : './junit-out.xml'; -export const MOCHA_CI_PROPERTIES: string = MOCHA_REPORTER_JUNIT && process.env.MOCHA_CI_PROPERTIES !== undefined ? - process.env.MOCHA_CI_PROPERTIES : ''; -export const MOCHA_CI_REPORTER_ID: string = MOCHA_REPORTER_JUNIT && process.env.MOCHA_CI_REPORTER_ID !== undefined ? - process.env.MOCHA_CI_REPORTER_ID : 'mocha-junit-reporter'; export const IS_CI_SERVER_TEST_DEBUGGER = process.env.IS_CI_SERVER_TEST_DEBUGGER === '1'; diff --git a/src/test/common.ts b/src/test/common.ts index 1bd952350524..886323e815a5 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -2,69 +2,75 @@ // 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 arch from 'arch'; 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 { IPythonSettings, Resource } from '../client/common/types'; -import { PythonInterpreter } from '../client/interpreter/contracts'; +import { IDisposable } from '../client/common/types'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; -import { - EXTENSION_ROOT_DIR_FOR_TESTS, IS_MULTI_ROOT_TEST, IS_PERF_TEST, IS_SMOKE_TEST -} from './constants'; +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'; 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(); +const arch = require('arch'); export const IS_64_BIT = arch() === 'x64'; export enum OSType { Unknown = 'Unknown', Windows = 'Windows', OSX = 'OSX', - Linux = 'Linux' + Linux = 'Linux', } -export type PythonSettingKeys = 'workspaceSymbols.enabled' | 'pythonPath' | - 'linting.lintOnSave' | - 'linting.enabled' | 'linting.pylintEnabled' | - 'linting.flake8Enabled' | 'linting.pep8Enabled' | 'linting.pylamaEnabled' | - 'linting.prospectorEnabled' | 'linting.pydocstyleEnabled' | 'linting.mypyEnabled' | 'linting.banditEnabled' | - 'unitTest.nosetestArgs' | 'unitTest.pyTestArgs' | 'unitTest.unittestArgs' | - 'formatting.provider' | 'sortImports.args' | - 'unitTest.nosetestsEnabled' | 'unitTest.pyTestEnabled' | 'unitTest.unittestEnabled' | - 'envFile' | 'jediEnabled' | 'linting.ignorePatterns' | 'terminal.activateEnvironment'; +export type PythonSettingKeys = + | 'defaultInterpreterPath' + | 'languageServer' + | 'testing.pytestArgs' + | 'testing.unittestArgs' + | 'formatting.provider' + | 'testing.pytestEnabled' + | 'testing.unittestEnabled' + | 'envFile' + | '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(); } } -export async function updateSetting(setting: PythonSettingKeys, value: {} | undefined, resource: Uri | undefined, configTarget: ConfigurationTarget) { +export async function updateSetting( + setting: PythonSettingKeys, + value: {} | undefined, + resource: Uri | undefined, + configTarget: ConfigurationTarget, +) { const vscode = require('vscode') as typeof import('vscode'); - const settings = vscode.workspace.getConfiguration('python', resource); + const settings = vscode.workspace.getConfiguration('python', { uri: resource, languageId: 'python' }); const currentValue = settings.inspect(setting); - if (currentValue !== undefined && ((configTarget === vscode.ConfigurationTarget.Global && currentValue.globalValue === value) || - (configTarget === vscode.ConfigurationTarget.Workspace && currentValue.workspaceValue === value) || - (configTarget === vscode.ConfigurationTarget.WorkspaceFolder && currentValue.workspaceFolderValue === value))) { + if ( + currentValue !== undefined && + ((configTarget === vscode.ConfigurationTarget.Global && currentValue.globalValue === value) || + (configTarget === vscode.ConfigurationTarget.Workspace && currentValue.workspaceValue === value) || + (configTarget === vscode.ConfigurationTarget.WorkspaceFolder && + currentValue.workspaceFolderValue === value)) + ) { await disposePythonSettings(); return; } @@ -95,8 +101,31 @@ export async function restorePythonPathInWorkspaceRoot() { return retryAsync(setPythonPathInWorkspace)(undefined, vscode.ConfigurationTarget.Workspace, PYTHON_PATH); } +export async function setGlobalInterpreterPath(pythonPath: string) { + return retryAsync(setGlobalPathToInterpreter)(pythonPath); +} + +export const resetGlobalInterpreterPathSetting = async () => retryAsync(restoreGlobalInterpreterPathSetting)(); + +async function restoreGlobalInterpreterPathSetting(): Promise<void> { + const vscode = require('vscode') as typeof import('vscode'); + const pythonConfig = vscode.workspace.getConfiguration('python', (null as any) as Uri); + await pythonConfig.update('defaultInterpreterPath', undefined, true); + await disposePythonSettings(); +} +async function setGlobalPathToInterpreter(pythonPath?: string): Promise<void> { + const vscode = require('vscode') as typeof import('vscode'); + const pythonConfig = vscode.workspace.getConfiguration('python', (null as any) as Uri); + await pythonConfig.update('defaultInterpreterPath', pythonPath, true); + await disposePythonSettings(); +} export const resetGlobalPythonPathSetting = async () => retryAsync(restoreGlobalPythonPathSetting)(); +export async function setAutoSaveDelayInWorkspaceRoot(delayinMS: number) { + const vscode = require('vscode') as typeof import('vscode'); + return retryAsync(setAutoSaveDelay)(undefined, vscode.ConfigurationTarget.Workspace, delayinMS); +} + function getWorkspaceRoot() { if (IS_SMOKE_TEST || IS_PERF_TEST) { return; @@ -112,41 +141,21 @@ 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(wrapped: Function, retryCount: number = 2) { +export function retryAsync(this: any, wrapped: Function, retryCount: number = 2) { return async (...args: any[]) => { return new Promise((resolve, reject) => { const reasons: any[] = []; const makeCall = () => { - wrapped.call(this as Function, ...args) - .then(resolve, (reason: any) => { - reasons.push(reason); - if (reasons.length >= retryCount) { - reject(reasons); - } else { - // If failed once, lets wait for some time before trying again. - setTimeout(makeCall, 500); - } - }); + wrapped.call(this as Function, ...args).then(resolve, (reason: any) => { + reasons.push(reason); + if (reasons.length >= retryCount) { + reject(reasons); + } else { + // If failed once, lets wait for some time before trying again. + setTimeout(makeCall, 500); + } + }); }; makeCall(); @@ -154,24 +163,48 @@ export function retryAsync(wrapped: Function, retryCount: number = 2) { }; } -async function setPythonPathInWorkspace(resource: string | Uri | undefined, config: ConfigurationTarget, pythonPath?: string) { +async function setAutoSaveDelay(resource: string | Uri | undefined, config: ConfigurationTarget, delayinMS: number) { + const vscode = require('vscode') as typeof import('vscode'); + if (config === vscode.ConfigurationTarget.WorkspaceFolder && !IS_MULTI_ROOT_TEST) { + return; + } + const resourceUri = typeof resource === 'string' ? vscode.Uri.file(resource) : resource; + const settings = vscode.workspace.getConfiguration('files', resourceUri || null); + const value = settings.inspect<number>('autoSaveDelay'); + const prop: 'workspaceFolderValue' | 'workspaceValue' = + config === vscode.ConfigurationTarget.Workspace ? 'workspaceValue' : 'workspaceFolderValue'; + if (value && value[prop] !== delayinMS) { + await settings.update('autoSaveDelay', delayinMS, config); + await settings.update('autoSave', 'afterDelay'); + } +} + +async function setPythonPathInWorkspace( + resource: string | Uri | undefined, + config: ConfigurationTarget, + pythonPath?: string, +) { const vscode = require('vscode') as typeof import('vscode'); if (config === vscode.ConfigurationTarget.WorkspaceFolder && !IS_MULTI_ROOT_TEST) { return; } const resourceUri = typeof resource === 'string' ? vscode.Uri.file(resource) : resource; - const settings = vscode.workspace.getConfiguration('python', resourceUri); - const value = settings.inspect<string>('pythonPath'); - const prop: 'workspaceFolderValue' | 'workspaceValue' = config === vscode.ConfigurationTarget.Workspace ? 'workspaceValue' : 'workspaceFolderValue'; + const settings = vscode.workspace.getConfiguration('python', resourceUri || null); + 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(); } } 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 pythonConfig.update('pythonPath', undefined, true); + const pythonConfig = vscode.workspace.getConfiguration('python', (null as any) as Uri); + await Promise.all([ + pythonConfig.update('defaultInterpreterPath', undefined, true), + pythonConfig.update('defaultInterpreterPath', undefined, true), + ]); await disposePythonSettings(); } @@ -191,15 +224,18 @@ 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))); + return Promise.all(items.map((item) => fs.remove(item).catch(noop))); } function getPythonPath(): string { if (process.env.CI_PYTHON_PATH && fs.existsSync(process.env.CI_PYTHON_PATH)) { return process.env.CI_PYTHON_PATH; } + + // TODO: Change this to python3. + // See https://github.com/microsoft/vscode-python/issues/10910. return 'python'; } @@ -231,6 +267,24 @@ export function getOSType(platform: string = process.platform): OSType { } } +/** + * Update a string that represents a path in any OS to the string representation of + * that same path in a different OS. Note: Does not handle drive letter if the path + * is intended for a root. + * + * @param pathToCorrect The string representation of a path from a specific OS. + * @param os The OS representation to switch to - if left undefined the current OS is used. + */ +export function correctPathForOsType(pathToCorrect: string, os?: OSType): string { + if (os === undefined) { + os = getOSType(); + } + const pathSep: string = os === OSType.Windows ? '\\' : '/'; + const replacePathSepRegex: RegExp = os === OSType.Windows ? /\//g : /\\/g; + + return pathToCorrect.replace(replacePathSepRegex, pathSep); +} + /** * Get the current Python interpreter version. * @@ -238,14 +292,14 @@ export function getOSType(platform: string = process.platform): OSType { * @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.exec(PYTHON_PATH, pyVerArgs) - .then(strVersion => new SemVer(strVersion.stdout.trim())) + return pythonProcRunner + .exec(PYTHON_PATH, pyVerArgs) + .then((strVersion) => new SemVer(strVersion.stdout.trim())) .catch((err) => { // if the call fails this should make it loudly apparent. console.error('Failed to get Python Version in getPythonSemVer', err); @@ -273,7 +327,7 @@ export async function getPythonSemVer(procService?: IProcessService): Promise<Se */ export function isVersionInList(version: SemVer, ...searchVersions: string[]): boolean { // see if the major/minor version matches any member of the skip-list. - const isPresent = searchVersions.findIndex(ver => { + const isPresent = searchVersions.findIndex((ver) => { const semverChecker = coerce(ver); if (semverChecker) { if (semverChecker.compare(version) === 0) { @@ -332,7 +386,9 @@ export async function isPythonVersionInProcess(procService?: IProcessService, .. if (currentPyVersion) { return isVersionInList(currentPyVersion, ...versions); } else { - console.error(`Failed to determine the current Python version when comparing against list [${versions.join(', ')}].`); + console.error( + `Failed to determine the current Python version when comparing against list [${versions.join(', ')}].`, + ); return false; } } @@ -363,12 +419,14 @@ export async function isPythonVersion(...versions: string[]): Promise<boolean> { if (currentPyVersion) { return isVersionInList(currentPyVersion, ...versions); } else { - console.error(`Failed to determine the current Python version when comparing against list [${versions.join(', ')}].`); + console.error( + `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; } @@ -378,10 +436,10 @@ 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 => { + zip.extract('extension', targetFolder, (err: any) => { if (err) { reject(err); } else { @@ -392,34 +450,138 @@ export async function unzip(zipFile: string, targetFolder: string): Promise<void }); }); } - -export async function waitForCondition(condition: () => Promise<boolean>, timeoutMs: number, errorMessage: string): Promise<void> { +/** + * Wait for a condition to be fulfilled within a timeout. + */ +export async function waitForCondition( + condition: () => Promise<boolean>, + timeoutMs: number, + errorMessage: string, +): Promise<void> { return new Promise<void>(async (resolve, reject) => { - let completed = false; const timeout = setTimeout(() => { - if (!completed) { - reject(new Error(errorMessage)); - } - completed = true; + clearTimeout(timeout); + + // eslint-disable-next-line @typescript-eslint/no-use-before-define + clearTimeout(timer); + reject(new Error(errorMessage)); }, timeoutMs); - for (let i = 0; i < timeoutMs / 1000; i += 1) { - if (await condition()) { - clearTimeout(timeout); - resolve(); - return; - } - await sleep(500); - if (completed) { + const timer = setInterval(async () => { + if (!(await condition().catch(() => false))) { return; } - } + clearTimeout(timeout); + clearTimeout(timer); + resolve(); + }, 10); }); } +/** + * Execute a method until it executes without any exceptions. + */ +export async function retryIfFail<T>(fn: () => Promise<T>, timeoutMs: number = 60_000): Promise<T> { + let lastEx: Error | undefined; + const started = new Date().getTime(); + while (timeoutMs > new Date().getTime() - started) { + try { + const result = await fn(); + // Capture result, if no exceptions return that. + return result; + } catch (ex) { + lastEx = ex as Error | undefined; + } + await sleep(10); + } + if (lastEx) { + throw lastEx; + } + throw new Error('Timeout waiting for function to complete without any errors'); +} + export async function openFile(file: string): Promise<TextDocument> { const vscode = require('vscode') as typeof import('vscode'); const textDocument = await vscode.workspace.openTextDocument(file); await vscode.window.showTextDocument(textDocument); - assert(vscode.window.activeTextEditor, 'No active editor'); + assert.ok(vscode.window.activeTextEditor, 'No active editor'); return textDocument; } + +/** + * Helper class to test events. + * + * Usage: Assume xyz.onDidSave is the event we want to test. + * const handler = new TestEventHandler(xyz.onDidSave); + * // Do something that would trigger the event. + * assert.ok(handler.fired) + * assert.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() { + return this.handledEvents.length > 0; + } + public get first(): T { + return this.handledEvents[0]; + } + public get second(): T { + return this.handledEvents[1]; + } + public get last(): T { + return this.handledEvents[this.handledEvents.length - 1]; + } + public get count(): number { + return this.handledEvents.length; + } + public get all(): T[] { + return this.handledEvents; + } + private readonly handler: IDisposable; + + private readonly handledEvents: any[] = []; + constructor(event: Event<T>, private readonly eventNameForErrorMessages: string, disposables: IDisposable[] = []) { + disposables.push(this); + this.handler = event(this.listener, this); + } + public reset() { + while (this.handledEvents.length) { + this.handledEvents.pop(); + } + } + public async assertFired(waitPeriod: number = 100): Promise<void> { + await waitForCondition(async () => this.fired, waitPeriod, `${this.eventNameForErrorMessages} event not fired`); + } + public async assertFiredExactly(numberOfTimesFired: number, waitPeriod: number = 2_000): Promise<void> { + await waitForCondition( + async () => this.count === numberOfTimesFired, + waitPeriod, + `${this.eventNameForErrorMessages} event fired ${this.count}, expected ${numberOfTimesFired}`, + ); + } + public async assertFiredAtLeast(numberOfTimesFired: number, waitPeriod: number = 2_000): Promise<void> { + await waitForCondition( + async () => this.count >= numberOfTimesFired, + waitPeriod, + `${this.eventNameForErrorMessages} event fired ${this.count}, expected at least ${numberOfTimesFired}.`, + ); + } + public atIndex(index: number): T { + return this.handledEvents[index]; + } + + public dispose() { + this.handler.dispose(); + } + + private listener(e: T) { + this.handledEvents.push(e); + } +} + +export function createEventHandler<T, K extends keyof T>( + obj: T, + eventName: K, + dispoables: IDisposable[] = [], +): T[K] extends Event<infer TArgs> ? TestEventHandler<TArgs> : TestEventHandler<void> { + 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 new file mode 100644 index 000000000000..dfcc6a4ad434 --- /dev/null +++ b/src/test/common/application/commands/reloadCommand.unit.test.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { ApplicationShell } from '../../../../client/common/application/applicationShell'; +import { CommandManager } from '../../../../client/common/application/commandManager'; +import { ReloadVSCodeCommandHandler } from '../../../../client/common/application/commands/reloadCommand'; +import { IApplicationShell, ICommandManager } from '../../../../client/common/application/types'; +import { Common } from '../../../../client/common/utils/localize'; + +// Defines a Mocha test suite to group tests of similar kind together +suite('Common Commands ReloadCommand', () => { + let reloadCommandHandler: ReloadVSCodeCommandHandler; + let appShell: IApplicationShell; + let cmdManager: ICommandManager; + setup(async () => { + appShell = mock(ApplicationShell); + cmdManager = mock(CommandManager); + reloadCommandHandler = new ReloadVSCodeCommandHandler(instance(cmdManager), instance(appShell)); + when(cmdManager.executeCommand(anything())).thenResolve(); + await reloadCommandHandler.activate(); + }); + + test('Confirm command handler is added', async () => { + verify(cmdManager.registerCommand('python.reloadVSCode', anything(), anything())).once(); + }); + test('Display prompt to reload VS Code with message passed into command', async () => { + const message = 'Hello World!'; + + const commandHandler = capture(cmdManager.registerCommand as any).first()[1] as Function; + + await commandHandler.call(reloadCommandHandler, message); + + verify(appShell.showInformationMessage(message, Common.reload)).once(); + }); + test('Do not reload VS Code if user selects `Reload` option', async () => { + const message = 'Hello World!'; + + const commandHandler = capture(cmdManager.registerCommand as any).first()[1] as Function; + + when(appShell.showInformationMessage(message, Common.reload)).thenResolve(Common.reload as any); + + await commandHandler.call(reloadCommandHandler, message); + + verify(appShell.showInformationMessage(message, Common.reload)).once(); + verify(cmdManager.executeCommand('workbench.action.reloadWindow')).once(); + }); + test('Do not reload VS Code if user does not select `Reload` option', async () => { + const message = 'Hello World!'; + + const commandHandler = capture(cmdManager.registerCommand as any).first()[1] as Function; + when(appShell.showInformationMessage(message, Common.reload)).thenResolve(); + + await commandHandler.call(reloadCommandHandler, message); + + verify(appShell.showInformationMessage(message, Common.reload)).once(); + verify(cmdManager.executeCommand('workbench.action.reloadWindow')).never(); + }); +}); diff --git a/src/test/common/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/configSettings.multiroot.test.ts b/src/test/common/configSettings.multiroot.test.ts deleted file mode 100644 index 988945d135ff..000000000000 --- a/src/test/common/configSettings.multiroot.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -import * as assert from 'assert'; -import * as path from 'path'; -import { ConfigurationTarget, Uri, workspace } from 'vscode'; -import { PythonSettings } from '../../client/common/configSettings'; -import { clearPythonPathInWorkspaceFolder, getExtensionSettings } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; - -const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); - -// tslint:disable-next-line:max-func-body-length -suite('Multiroot Config Settings', () => { - suiteSetup(async function () { - if (!IS_MULTI_ROOT_TEST) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - } - await clearPythonPathInWorkspaceFolder(Uri.file(path.join(multirootPath, 'workspace1'))); - await initialize(); - }); - setup(initializeTest); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await closeActiveWindows(); - await clearPythonPathInWorkspaceFolder(Uri.file(path.join(multirootPath, 'workspace1'))); - await initializeTest(); - }); - - async function enableDisableLinterSetting(resource: Uri, configTarget: ConfigurationTarget, setting: string, enabled: boolean | undefined): Promise<void> { - const settings = workspace.getConfiguration('python.linting', resource); - const cfgValue = settings.inspect<boolean>(setting); - if (configTarget === ConfigurationTarget.Workspace && cfgValue && cfgValue.workspaceValue === enabled) { - return; - } - if (configTarget === ConfigurationTarget.WorkspaceFolder && cfgValue && cfgValue.workspaceFolderValue === enabled) { - return; - } - await settings.update(setting, enabled, configTarget); - PythonSettings.dispose(); - } - - test('Workspace folder should inherit Python Path from workspace root', async () => { - const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); - let settings = workspace.getConfiguration('python', workspaceUri); - const pythonPath = `x${new Date().getTime()}`; - await settings.update('pythonPath', pythonPath, ConfigurationTarget.Workspace); - const value = settings.inspect('pythonPath'); - if (value && typeof value.workspaceFolderValue === 'string') { - await settings.update('pythonPath', undefined, ConfigurationTarget.WorkspaceFolder); - } - settings = workspace.getConfiguration('python', workspaceUri); - PythonSettings.dispose(); - const cfgSetting = getExtensionSettings(workspaceUri); - assert.equal(cfgSetting.pythonPath, pythonPath, 'Python Path not inherited from workspace'); - }); - - test('Workspace folder should not inherit Python Path from workspace root', async () => { - const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); - const settings = workspace.getConfiguration('python', workspaceUri); - const pythonPath = `x${new Date().getTime()}`; - await settings.update('pythonPath', pythonPath, ConfigurationTarget.Workspace); - const privatePythonPath = `x${new Date().getTime()}`; - await settings.update('pythonPath', privatePythonPath, ConfigurationTarget.WorkspaceFolder); - - const cfgSetting = getExtensionSettings(workspaceUri); - assert.equal(cfgSetting.pythonPath, privatePythonPath, 'Python Path for workspace folder is incorrect'); - }); - - test('Workspace folder should inherit Python Path from workspace root when opening a document', async () => { - const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); - const fileToOpen = path.join(multirootPath, 'workspace1', 'file.py'); - - const settings = workspace.getConfiguration('python', workspaceUri); - const pythonPath = `x${new Date().getTime()}`; - await settings.update('pythonPath', pythonPath, ConfigurationTarget.Workspace); - // Update workspace folder to something else so it gets refreshed. - await settings.update('pythonPath', `x${new Date().getTime()}`, ConfigurationTarget.WorkspaceFolder); - await settings.update('pythonPath', undefined, ConfigurationTarget.WorkspaceFolder); - - const document = await workspace.openTextDocument(fileToOpen); - const cfg = getExtensionSettings(document.uri); - assert.equal(cfg.pythonPath, pythonPath, 'Python Path not inherited from workspace'); - }); - - test('Workspace folder should not inherit Python Path from workspace root when opening a document', async () => { - const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); - const fileToOpen = path.join(multirootPath, 'workspace1', 'file.py'); - - const settings = workspace.getConfiguration('python', workspaceUri); - const pythonPath = `x${new Date().getTime()}`; - await settings.update('pythonPath', pythonPath, ConfigurationTarget.Workspace); - const privatePythonPath = `x${new Date().getTime()}`; - await settings.update('pythonPath', privatePythonPath, ConfigurationTarget.WorkspaceFolder); - - const document = await workspace.openTextDocument(fileToOpen); - const cfg = getExtensionSettings(document.uri); - assert.equal(cfg.pythonPath, privatePythonPath, 'Python Path for workspace folder is incorrect'); - }); - - test('Enabling/Disabling Pylint in root should be reflected in config settings', async () => { - const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', undefined); - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); - let settings = getExtensionSettings(workspaceUri); - assert.equal(settings.linting.pylintEnabled, true, 'Pylint not enabled when it should be'); - - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); - settings = getExtensionSettings(workspaceUri); - assert.equal(settings.linting.pylintEnabled, false, 'Pylint enabled when it should not be'); - }); - - test('Enabling/Disabling Pylint in root and workspace should be reflected in config settings', async () => { - const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); - - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); - - let cfgSetting = getExtensionSettings(workspaceUri); - assert.equal(cfgSetting.linting.pylintEnabled, false, 'Workspace folder pylint setting is true when it should not be'); - PythonSettings.dispose(); - - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); - - cfgSetting = getExtensionSettings(workspaceUri); - assert.equal(cfgSetting.linting.pylintEnabled, true, 'Workspace folder pylint setting is false when it should not be'); - }); - - test('Enabling/Disabling Pylint in root should be reflected in config settings when opening a document', async () => { - const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); - const fileToOpen = path.join(multirootPath, 'workspace1', 'file.py'); - - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); - let document = await workspace.openTextDocument(fileToOpen); - let cfg = getExtensionSettings(document.uri); - assert.equal(cfg.linting.pylintEnabled, true, 'Pylint should be enabled in workspace'); - PythonSettings.dispose(); - - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); - document = await workspace.openTextDocument(fileToOpen); - cfg = getExtensionSettings(document.uri); - assert.equal(cfg.linting.pylintEnabled, false, 'Pylint should not be enabled in workspace'); - }); - - test('Enabling/Disabling Pylint in root should be reflected in config settings when opening a document', async () => { - const workspaceUri = Uri.file(path.join(multirootPath, 'workspace1')); - const fileToOpen = path.join(multirootPath, 'workspace1', 'file.py'); - - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', false); - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', true); - let document = await workspace.openTextDocument(fileToOpen); - let cfg = getExtensionSettings(document.uri); - assert.equal(cfg.linting.pylintEnabled, true, 'Pylint should be enabled in workspace'); - PythonSettings.dispose(); - - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.Workspace, 'pylintEnabled', true); - await enableDisableLinterSetting(workspaceUri, ConfigurationTarget.WorkspaceFolder, 'pylintEnabled', false); - document = await workspace.openTextDocument(fileToOpen); - cfg = getExtensionSettings(document.uri); - assert.equal(cfg.linting.pylintEnabled, false, 'Pylint should not be enabled in workspace'); - }); - - // tslint:disable-next-line:no-invalid-template-strings - test('${workspaceFolder} variable in settings should be replaced with the right value', async () => { - const workspace2Uri = Uri.file(path.join(multirootPath, 'workspace2')); - let fileToOpen = path.join(workspace2Uri.fsPath, 'file.py'); - - let document = await workspace.openTextDocument(fileToOpen); - let cfg = getExtensionSettings(document.uri); - assert.equal(path.dirname(cfg.workspaceSymbols.tagFilePath), workspace2Uri.fsPath, 'ctags file path for workspace2 is incorrect'); - assert.equal(path.basename(cfg.workspaceSymbols.tagFilePath), 'workspace2.tags.file', 'ctags file name for workspace2 is incorrect'); - PythonSettings.dispose(); - - const workspace3Uri = Uri.file(path.join(multirootPath, 'workspace3')); - fileToOpen = path.join(workspace3Uri.fsPath, 'file.py'); - - document = await workspace.openTextDocument(fileToOpen); - cfg = getExtensionSettings(document.uri); - assert.equal(path.dirname(cfg.workspaceSymbols.tagFilePath), workspace3Uri.fsPath, 'ctags file path for workspace3 is incorrect'); - assert.equal(path.basename(cfg.workspaceSymbols.tagFilePath), 'workspace3.tags.file', 'ctags file name for workspace3 is incorrect'); - PythonSettings.dispose(); - }); -}); diff --git a/src/test/common/configSettings.test.ts b/src/test/common/configSettings.test.ts index 79ebd878f49b..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'); @@ -13,12 +12,12 @@ const workspaceRoot = path.join(__dirname, '..', '..', '..', 'src', 'test'); suite('Configuration Settings', () => { setup(initialize); - test('Check Values', done => { - const systemVariables: SystemVariables = new SystemVariables(workspaceRoot); - // tslint:disable-next-line:no-any - const pythonConfig = vscode.workspace.getConfiguration('python', null as any as vscode.Uri); + test('Check Values', (done) => { + const systemVariables: SystemVariables = new SystemVariables(undefined, workspaceRoot); + + const pythonConfig = vscode.workspace.getConfiguration('python', (null as any) as vscode.Uri); const pythonSettings = getExtensionSettings(vscode.Uri.file(workspaceRoot)); - Object.keys(pythonSettings).forEach(key => { + Object.keys(pythonSettings).forEach((key) => { let settingValue = pythonConfig.get(key, 'Not a config'); if (settingValue === 'Not a config') { return; @@ -26,20 +25,14 @@ suite('Configuration Settings', () => { if (settingValue) { settingValue = systemVariables.resolve(settingValue); } - // tslint:disable-next-line:no-any - const pythonSettingValue = (pythonSettings[key] as string); - if (key.endsWith('Path') && IS_WINDOWS) { - assert.equal(settingValue.toUpperCase(), pythonSettingValue.toUpperCase(), `Setting ${key} not the same`); - } else if (key === 'workspaceSymbols' && IS_WINDOWS) { - const workspaceSettings = (pythonSettingValue as {} as IWorkspaceSymbolSettings); - const workspaceSttings = (settingValue as {} as IWorkspaceSymbolSettings); - assert.equal(workspaceSettings.tagFilePath.toUpperCase(), workspaceSttings.tagFilePath.toUpperCase(), `Setting ${key} not the same`); - const workspaceSettingsWithoutPath = { ...workspaceSettings }; - delete workspaceSettingsWithoutPath.tagFilePath; - const pythonSettingValueWithoutPath = { ...(pythonSettingValue as {} as IWorkspaceSymbolSettings) }; - delete pythonSettingValueWithoutPath.tagFilePath; - assert.deepEqual(workspaceSettingsWithoutPath, pythonSettingValueWithoutPath, `Setting ${key} not the same`); + const pythonSettingValue = (pythonSettings as any)[key] as string; + if (key.endsWith('Path') && isWindows()) { + assert.strictEqual( + settingValue.toUpperCase(), + pythonSettingValue.toUpperCase(), + `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 1e2ade46739d..8a2a90b288a3 100644 --- a/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts +++ b/src/test/common/configSettings/configSettings.pythonPath.unit.test.ts @@ -3,111 +3,224 @@ 'use strict'; -// tslint:disable:no-require-imports no-var-requires max-func-body-length no-unnecessary-override no-invalid-template-strings no-any - import { expect } from 'chai'; import * as path from 'path'; +import * as sinon from 'sinon'; import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; import { Uri, WorkspaceConfiguration } from 'vscode'; -import { - PythonSettings -} from '../../../client/common/configSettings'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { IExperimentService, IInterpreterPathService } from '../../../client/common/types'; import { noop } from '../../../client/common/utils/misc'; +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() { noop(); } + + // eslint-disable-next-line class-methods-use-this + public initialize() { + noop(); + } } let configSettings: CustomPythonSettings; + let workspaceService: typemoq.IMock<IWorkspaceService>; + 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<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' })); }); teardown(() => { if (configSettings) { configSettings.dispose(); } + 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')); }); - test('If we don\'t have a custom python path and no auto selected interpreters, then use default', () => { + 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 = { 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(); + interpreterPathService.setup((p) => p.get(typemoq.It.isAny())).returns(() => 'python'); + configSettings = new CustomPythonSettings( + workspaceFolderUri, + instance(selectionService), + workspaceService.object, + interpreterPathService.object, + undefined, + new MockExtensions(), + ); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(pythonPath); + verify(selectionService.setWorkspaceInterpreter(workspaceFolderUri, interpreter)).once(); // Verify we set the autoselected interpreter + }); + 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); 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)); + interpreterPathService.setup((p) => p.get(typemoq.It.isAny())).returns(() => 'python'); + + configSettings = new CustomPythonSettings( + workspaceFolderUri, + instance(selectionService), + workspaceService.object, + interpreterPathService.object, + undefined, + new MockExtensions(), + ); + configSettings.update(pythonSettings.object); + + expect(configSettings.pythonPath).to.be.equal(pythonPath); + }); + 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( + workspaceFolderUri, + instance(selectionService), + workspaceService.object, + interpreterPathService.object, + undefined, + new MockExtensions(), + ); + 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.defaultInterpreterPath).to.be.equal(pythonPath); + }); + 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, + instance(selectionService), + workspaceService.object, + interpreterPathService.object, + undefined, + new MockExtensions(), + ); + interpreterPathService.setup((i) => i.get(resource)).returns(() => 'python'); configSettings.update(pythonSettings.object); expect(configSettings.pythonPath).to.be.equal(pythonPath); - verify(selectionService.getAutoSelectedInterpreter(workspaceFolderUri)).once(); + experimentsManager.verifyAll(); + interpreterPathService.verifyAll(); + pythonSettings.verifyAll(); }); }); diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index e571f73ed049..65afc782d7bb 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -5,107 +5,182 @@ 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 { - PythonSettings -} from '../../../client/common/configSettings'; -import { - IAnalysisSettings, IAutoCompleteSettings, - IDataScienceSettings, - IFormattingSettings, - ILintingSettings, - ISortImportSettings, + IExperiments, + IInterpreterSettings, ITerminalSettings, - IUnitTestSettings, - 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', () => { +suite('Python Settings', async () => { class CustomPythonSettings extends PythonSettings { - // tslint:disable-next-line:no-unnecessary-override public update(pythonSettings: WorkspaceConfiguration) { return super.update(pythonSettings); } - protected initialize() { noop(); } + public initialize() { + noop(); + } } let config: TypeMoq.IMock<WorkspaceConfiguration>; let expected: CustomPythonSettings; let settings: CustomPythonSettings; + let extensions: MockExtensions; setup(() => { - config = TypeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, TypeMoq.MockBehavior.Strict); - expected = new CustomPythonSettings(undefined, new MockAutoSelectionService()); - settings = new CustomPythonSettings(undefined, new MockAutoSelectionService()); + sinon.stub(EnvFileTelemetry, 'sendSettingTelemetry').returns(); + 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'; + }); + + teardown(() => { + sinon.restore(); }); function initializeConfig(sourceSettings: PythonSettings) { // string settings - for (const name of ['pythonPath', 'venvPath', 'condaPath', 'envFile']) { - config.setup(c => c.get<string>(name)) - .returns(() => sourceSettings[name]); - } - if (sourceSettings.jediEnabled) { - config.setup(c => c.get<string>('jediPath')) - .returns(() => sourceSettings.jediPath); + for (const name of [ + 'pythonPath', + 'venvPath', + 'activeStateToolPath', + 'condaPath', + 'pipenvPath', + 'envFile', + 'poetryPath', + 'pixiToolPath', + 'defaultInterpreterPath', + ]) { + config + .setup((c) => c.get<string>(name)) + + .returns(() => (sourceSettings as any)[name]); } for (const name of ['venvFolders']) { - config.setup(c => c.get<string[]>(name)) - .returns(() => sourceSettings[name]); + config + .setup((c) => c.get<string[]>(name)) + + .returns(() => (sourceSettings as any)[name]); } // boolean settings - for (const name of ['downloadLanguageServer', 'jediEnabled', 'autoUpdateLanguageServer']) { - config.setup(c => c.get<boolean>(name, true)) - .returns(() => sourceSettings[name]); - } - for (const name of ['disableInstallationCheck', 'globalModuleInstallation']) { - config.setup(c => c.get<boolean>(name)) - .returns(() => sourceSettings[name]); - } + for (const name of ['globalModuleInstallation']) { + config + .setup((c) => c.get<boolean>(name)) - // number settings - if (sourceSettings.jediEnabled) { - config.setup(c => c.get<number>('jediMemoryLimit')) - .returns(() => sourceSettings.jediMemoryLimit); + .returns(() => (sourceSettings as any)[name]); } + // 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); + + config.setup((c) => c.get<any[]>('devOptions')).returns(() => sourceSettings.devOptions); // complex settings - config.setup(c => c.get<ILintingSettings>('linting')) - .returns(() => sourceSettings.linting); - config.setup(c => c.get<IAnalysisSettings>('analysis')) - .returns(() => sourceSettings.analysis); - config.setup(c => c.get<ISortImportSettings>('sortImports')) - .returns(() => sourceSettings.sortImports); - config.setup(c => c.get<IFormattingSettings>('formatting')) - .returns(() => sourceSettings.formatting); - config.setup(c => c.get<IAutoCompleteSettings>('autoComplete')) - .returns(() => sourceSettings.autoComplete); - config.setup(c => c.get<IWorkspaceSymbolSettings>('workspaceSymbols')) - .returns(() => sourceSettings.workspaceSymbols); - config.setup(c => c.get<IUnitTestSettings>('unitTest')) - .returns(() => sourceSettings.unitTest); - 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<IInterpreterSettings>('interpreter')).returns(() => sourceSettings.interpreter); + config.setup((c) => c.get<IAutoCompleteSettings>('autoComplete')).returns(() => sourceSettings.autoComplete); + 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<IExperiments>('experiments')).returns(() => sourceSettings.experiments); } + function testIfValueIsUpdated(settingName: string, value: any) { + test(`${settingName} updated`, async () => { + expected.pythonPath = 'python3'; + (expected as any)[settingName] = value; + initializeConfig(expected); + + settings.update(config.object); + + expect((settings as any)[settingName]).to.be.equal((expected as any)[settingName]); + config.verifyAll(); + }); + } + + suite('String settings', async () => { + [ + 'venvPath', + 'activeStateToolPath', + 'condaPath', + 'pipenvPath', + 'envFile', + 'poetryPath', + 'pixiToolPath', + 'defaultInterpreterPath', + ].forEach(async (settingName) => { + testIfValueIsUpdated(settingName, 'stringValue'); + }); + }); + + suite('Boolean settings', async () => { + ['globalModuleInstallation'].forEach(async (settingName) => { + testIfValueIsUpdated(settingName, true); + }); + }); + + 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', () => { expected.pythonPath = 'python3'; expected.condaPath = 'spam'; initializeConfig(expected); - config.setup(c => c.get<string>('condaPath')) + config + .setup((c) => c.get<string>('condaPath')) .returns(() => expected.condaPath) .verifiable(TypeMoq.Times.once()); @@ -115,11 +190,12 @@ suite('Python Settings', () => { config.verifyAll(); }); - test('condaPath (relative to home) updated', () => { + test('condaPath (relative to home) updated', async () => { expected.pythonPath = 'python3'; expected.condaPath = path.join('~', 'anaconda3', 'bin', 'conda'); initializeConfig(expected); - config.setup(c => c.get<string>('condaPath')) + config + .setup((c) => c.get<string>('condaPath')) .returns(() => expected.condaPath) .verifiable(TypeMoq.Times.once()); @@ -129,52 +205,107 @@ suite('Python Settings', () => { config.verifyAll(); }); - 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()); + function testLanguageServer( + languageServer: LanguageServerType, + expectedValue: LanguageServerType, + isDefault: boolean, + ) { + test(languageServer, () => { + expected.pythonPath = 'python3'; + expected.languageServer = languageServer; + initializeConfig(expected); + config + .setup((c) => c.get<LanguageServerType>('languageServer')) + .returns(() => expected.languageServer) + .verifiable(TypeMoq.Times.once()); - settings.update(config.object); + settings.update(config.object); - for (const key of Object.keys(expected.formatting)) { - expect(settings.formatting[key]).to.be.deep.equal(expected.formatting[key]); - } - config.verifyAll(); + expect(settings.languageServer).to.be.equal(expectedValue); + expect(settings.languageServerIsDefault).to.be.equal(isDefault); + config.verifyAll(); + }); + } + + suite('languageServer settings', async () => { + 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, 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); + }); }); - test('Formatter Paths (paths relative to home)', () => { + + function testExperiments(enabled: boolean) { 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.experiments = { + enabled, + optInto: [], + optOutFrom: [], }; - expected.formatting.blackPath = 'spam'; initializeConfig(expected); - config.setup(c => c.get<IFormattingSettings>('formatting')) - .returns(() => expected.formatting) + config + .setup((c) => c.get<IExperiments>('experiments')) + .returns(() => expected.experiments) .verifiable(TypeMoq.Times.once()); settings.update(config.object); - for (const key of Object.keys(expected.formatting)) { - if (!key.endsWith('path')) { - continue; - } - const expectedPath = untildify(expected.formatting[key]); - expect(settings.formatting[key]).to.be.equal(expectedPath); + for (const key of Object.keys(expected.experiments)) { + expect((settings.experiments as any)[key]).to.be.deep.equal((expected.experiments as any)[key]); } config.verifyAll(); - }); + } + test('Experiments (not enabled)', () => testExperiments(false)); + + test('Experiments (enabled)', () => testExperiments(true)); }); diff --git a/src/test/common/configuration/service.test.ts b/src/test/common/configuration/service.test.ts index 1a81abe81c0c..c57617b2a610 100644 --- a/src/test/common/configuration/service.test.ts +++ b/src/test/common/configuration/service.test.ts @@ -2,39 +2,37 @@ // Licensed under the MIT License. import { expect } from 'chai'; import { workspace } from 'vscode'; -import { IAsyncDisposableRegistry, IConfigurationService, IDisposable } from '../../../client/common/types'; -import { getExtensionSettings } from '../../common'; +import { IConfigurationService, IDisposableRegistry, IExtensionContext } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { getExtensionSettings } from '../../extensionSettings'; import { initialize } from '../../initialize'; -import { UnitTestIocContainer } from '../../unittests/serviceRegistry'; -// tslint:disable-next-line:max-func-body-length suite('Configuration Service', () => { - let ioc: UnitTestIocContainer; - suiteSetup(initialize); - setup(() => { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); + let serviceContainer: IServiceContainer; + suiteSetup(async () => { + serviceContainer = (await initialize()).serviceContainer; }); - teardown(() => ioc.dispose()); - test('Ensure same instance of settings return', () => { + test('Ensure same instance of settings return', () => { const workspaceUri = workspace.workspaceFolders![0].uri; - const settings = ioc.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(workspaceUri); + const settings = serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(workspaceUri); const instanceIsSame = settings === getExtensionSettings(workspaceUri); expect(instanceIsSame).to.be.equal(true, 'Incorrect settings'); }); test('Ensure async registry works', async () => { - const asyncRegistry = ioc.serviceContainer.get<IAsyncDisposableRegistry>(IAsyncDisposableRegistry); - let disposed = false; - const disposable : IDisposable = { - dispose() : Promise<void> { - disposed = true; + const asyncRegistry = serviceContainer.get<IDisposableRegistry>(IDisposableRegistry); + let subs = serviceContainer.get<IExtensionContext>(IExtensionContext).subscriptions; + const oldLength = subs.length; + const disposable = { + dispose(): Promise<void> { 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 new file mode 100644 index 000000000000..19f57173f10a --- /dev/null +++ b/src/test/common/configuration/service.unit.test.ts @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { 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 serviceContainer: TypeMoq.IMock<IServiceContainer>; + let configService: ConfigurationService; + setup(() => { + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + workspaceService + .setup((w) => w.getWorkspaceFolder(resource)) + .returns(() => ({ + uri: resource, + index: 0, + name: '0', + })); + interpreterPathService = TypeMoq.Mock.ofType<IInterpreterPathService>(); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + serviceContainer.setup((s) => s.get(IWorkspaceService)).returns(() => workspaceService.object); + serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); + configService = new ConfigurationService(serviceContainer.object); + }); + + function setupConfigProvider(): TypeMoq.IMock<WorkspaceConfiguration> { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))) + .returns(() => workspaceConfig.object); + return workspaceConfig; + } + + test('Fetching settings goes as expected', () => { + const interpreterAutoSelectionProxyService = TypeMoq.Mock.ofType<IInterpreterAutoSelectionService>(); + serviceContainer + .setup((s) => s.get(IInterpreterAutoSelectionService)) + .returns(() => interpreterAutoSelectionProxyService.object) + .verifiable(TypeMoq.Times.once()); + const settings = configService.getSettings(); + expect(settings).to.be.instanceOf(PythonSettings); + }); + + test('Do not update global settings if global value is already equal to the new value', async () => { + const workspaceConfig = setupConfigProvider(); + + workspaceConfig + .setup((w) => w.inspect('setting')) + .returns(() => ({ globalValue: 'globalValue', key: 'setting' })); + workspaceConfig + .setup((w) => w.update('setting', 'globalValue', ConfigurationTarget.Global)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await configService.updateSetting('setting', 'globalValue', resource, ConfigurationTarget.Global); + + workspaceConfig.verifyAll(); + }); + + test('Update global settings if global value is not equal to the new value', async () => { + const workspaceConfig = setupConfigProvider(); + + workspaceConfig + .setup((w) => w.inspect('setting')) + .returns(() => ({ globalValue: 'globalValue', key: 'setting' })); + workspaceConfig + .setup((w) => w.update('setting', 'newGlobalValue', ConfigurationTarget.Global)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await configService.updateSetting('setting', 'newGlobalValue', resource, ConfigurationTarget.Global); + + workspaceConfig.verifyAll(); + }); + + test('Do not update workspace settings if workspace value is already equal to the new value', async () => { + const workspaceConfig = setupConfigProvider(); + + workspaceConfig + .setup((w) => w.inspect('setting')) + .returns(() => ({ workspaceValue: 'workspaceValue', key: 'setting' })); + workspaceConfig + .setup((w) => w.update('setting', 'workspaceValue', ConfigurationTarget.Workspace)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await configService.updateSetting('setting', 'workspaceValue', resource, ConfigurationTarget.Workspace); + + workspaceConfig.verifyAll(); + }); + + test('Update workspace settings if workspace value is not equal to the new value', async () => { + const workspaceConfig = setupConfigProvider(); + + workspaceConfig + .setup((w) => w.inspect('setting')) + .returns(() => ({ workspaceValue: 'workspaceValue', key: 'setting' })); + workspaceConfig + .setup((w) => w.update('setting', 'newWorkspaceValue', ConfigurationTarget.Workspace)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await configService.updateSetting('setting', 'newWorkspaceValue', resource, ConfigurationTarget.Workspace); + + workspaceConfig.verifyAll(); + }); + + test('Do not update workspace folder settings if workspace folder value is already equal to the new value', async () => { + const workspaceConfig = setupConfigProvider(); + workspaceConfig + .setup((w) => w.inspect('setting')) + + .returns(() => ({ workspaceFolderValue: 'workspaceFolderValue', key: 'setting' })); + workspaceConfig + .setup((w) => w.update('setting', 'workspaceFolderValue', ConfigurationTarget.WorkspaceFolder)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await configService.updateSetting( + 'setting', + 'workspaceFolderValue', + resource, + ConfigurationTarget.WorkspaceFolder, + ); + + workspaceConfig.verifyAll(); + }); + + test('Update workspace folder settings if workspace folder value is not equal to the new value', async () => { + const workspaceConfig = setupConfigProvider(); + workspaceConfig + .setup((w) => w.inspect('setting')) + + .returns(() => ({ workspaceFolderValue: 'workspaceFolderValue', key: 'setting' })); + workspaceConfig + .setup((w) => w.update('setting', 'newWorkspaceFolderValue', ConfigurationTarget.WorkspaceFolder)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await configService.updateSetting( + 'setting', + 'newWorkspaceFolderValue', + resource, + ConfigurationTarget.WorkspaceFolder, + ); + + workspaceConfig.verifyAll(); + }); +}); 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 8022ac8eebe8..000000000000 --- a/src/test/common/dotnet/compatibilityService.unit.test.ts +++ /dev/null @@ -1,44 +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/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 new file mode 100644 index 000000000000..cb04d3a90b38 --- /dev/null +++ b/src/test/common/exitCIAfterTestReporter.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// Custom reporter to ensure Mocha process exits when we're done with tests. +// This is a hack, however for some reason the process running the tests do not exit. +// The hack is to force it to die when tests are done, if this doesn't work we've got a bigger problem on our hands. + +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'; +import { noop } from '../core'; + +let client: net.Socket | undefined; +const mochaTests: any = require('mocha'); +const { EVENT_RUN_BEGIN, EVENT_RUN_END } = mochaTests.Runner.constants; + +async function connectToServer() { + const portFile = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'port.txt'); + if (!(await fs.pathExists(portFile))) { + return; + } + const port = parseInt(await fs.readFile(portFile, 'utf-8'), 10); + console.log(`Need to connect to port ${port}`); + return new Promise<void>((resolve) => { + try { + client = new net.Socket(); + client.connect({ port }, () => { + console.log(`Connected to port ${port}`); + resolve(); + }); + } catch { + console.error('Failed to connect to socket server to notify completion of tests'); + resolve(); + } + }); +} +function notifyCompleted(hasFailures: boolean) { + if (!client || client.destroyed || !client.writable) { + console.error('No client to write from'); + return; + } + try { + const exitCode = hasFailures ? 1 : 0; + console.log(`Notify server of test completion with code ${exitCode}`); + // If there are failures, send a code of 1 else 0. + client.write(exitCode.toString()); + client.end(); + console.log('Notified server of test completion'); + } catch (ex) { + console.error('Socket client error', ex); + } +} + +class ExitReporter { + constructor(runner: any) { + console.log('Initialize Exit Reporter for Mocha (PVSC).'); + connectToServer().catch(noop); + const stats = runner.stats; + runner + .once(EVENT_RUN_BEGIN, () => { + console.info('Start Exit Reporter for Mocha.'); + }) + .once(EVENT_RUN_END, async () => { + notifyCompleted(stats.failures > 0); + console.info('End Exit Reporter for Mocha.'); + }); + } +} + +module.exports = ExitReporter; diff --git a/src/test/common/experiments/service.unit.test.ts b/src/test/common/experiments/service.unit.test.ts new file mode 100644 index 000000000000..661efeaa8bb9 --- /dev/null +++ b/src/test/common/experiments/service.unit.test.ts @@ -0,0 +1,623 @@ +/* eslint-disable no-new */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { 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 { 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 { 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'; +import { MockOutputChannel } from '../../mockClasses'; +import { MockMemento } from '../../mocks/mementos'; + +suite('Experimentation service', () => { + const extensionVersion = '1.2.3'; + const dummyExperimentKey = 'experimentsKey'; + + let workspaceService: IWorkspaceService; + let appEnvironment: IApplicationEnvironment; + let stateFactory: IPersistentStateFactory; + let globalMemento: MockMemento; + let outputChannel: MockOutputChannel; + let disposeLogger: Disposable; + + setup(() => { + 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(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, contributes?: Record<string, unknown>) { + when(appEnvironment.channel).thenReturn(channel); + when(appEnvironment.extensionName).thenReturn(PVSC_EXTENSION_ID_FOR_TESTS); + when(appEnvironment.packageJson).thenReturn({ version, contributes }); + } + + suite('Initialization', () => { + 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); + + // 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, + sinon.match(TargetPopulation.Public), + sinon.match.any, + globalMemento, + ); + }); + + test('Users with VS Code Insiders version should be the Insiders target population', () => { + const getExperimentationServiceStub = sinon.stub(tasClient, 'getExperimentationService'); + + configureSettings(true, [], []); + configureApplicationEnvironment('insiders', extensionVersion); + + // eslint-disable-next-line no-new + new ExperimentService(instance(workspaceService), instance(appEnvironment), instance(stateFactory)); + + sinon.assert.calledWithExactly( + getExperimentationServiceStub, + PVSC_EXTENSION_ID_FOR_TESTS, + extensionVersion, + sinon.match(TargetPopulation.Insiders), + sinon.match.any, + globalMemento, + ); + }); + + test('Users can only opt into experiment groups', () => { + sinon.stub(tasClient, 'getExperimentationService'); + + configureSettings(true, ['Foo - experiment', 'Bar - control'], []); + configureApplicationEnvironment('stable', extensionVersion); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + + assert.deepEqual(experimentService._optInto, ['Foo - experiment']); + }); + + test('Users can only opt out of experiment groups', () => { + sinon.stub(tasClient, 'getExperimentationService'); + configureSettings(true, [], ['Foo - experiment', 'Bar - control']); + configureApplicationEnvironment('stable', extensionVersion); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + + assert.deepEqual(experimentService._optOutFrom, ['Foo - experiment']); + }); + + test('Experiment data in Memento storage should be logged if it starts with "python"', async () => { + const experiments = ['ExperimentOne', 'pythonExperiment']; + globalMemento.update(dummyExperimentKey, { features: experiments }); + configureSettings(true, [], []); + configureApplicationEnvironment('stable', extensionVersion, { configuration: { properties: {} } }); + + const exp = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + await exp.activate(); + const output = "Experiment 'pythonExperiment' is active\n"; + + assert.strictEqual(outputChannel.output, output); + }); + }); + + suite('In-experiment-sync check', () => { + const experiment = 'Test Experiment - experiment'; + let telemetryEvents: { eventName: string; properties: unknown }[] = []; + let getTreatmentVariable: sinon.SinonStub; + let sendTelemetryEventStub: sinon.SinonStub; + + setup(() => { + sendTelemetryEventStub = sinon + .stub(Telemetry, 'sendTelemetryEvent') + .callsFake((eventName: string, _, properties: unknown) => { + const telemetry = { eventName, properties }; + telemetryEvents.push(telemetry); + }); + + 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(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + const result = experimentService.inExperimentSync(experiment); + + assert.isTrue(result); + sinon.assert.notCalled(sendTelemetryEventStub); + 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(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 "All", inExperiment should return true', async () => { + configureSettings(true, ['All'], []); + + const experimentService = new ExperimentService( + instance(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + const result = experimentService.inExperimentSync(experiment); + + assert.isTrue(result); + 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(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + const result = experimentService.inExperimentSync(experiment); + + assert.isTrue(result); + 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(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 "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(workspaceService), + instance(appEnvironment), + instance(stateFactory), + ); + const result = experimentService.inExperimentSync(experiment); + + assert.isFalse(result); + 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']), + }); + }); + + // 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 new file mode 100644 index 000000000000..4c28e2ff4748 --- /dev/null +++ b/src/test/common/experiments/telemetry.unit.test.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { ExperimentationTelemetry } from '../../../client/common/experiments/telemetry'; +import * as Telemetry from '../../../client/telemetry'; + +suite('Experimentation telemetry', () => { + const event = 'SomeEventName'; + + let telemetryEvents: { eventName: string; properties: object }[] = []; + let sendTelemetryEventStub: sinon.SinonStub; + let setSharedPropertyStub: sinon.SinonStub; + let experimentTelemetry: ExperimentationTelemetry; + let eventProperties: Map<string, string>; + + setup(() => { + sendTelemetryEventStub = sinon.stub(Telemetry, 'sendTelemetryEvent').callsFake((( + eventName: string, + _, + properties: object, + ) => { + const telemetry = { eventName, properties }; + telemetryEvents.push(telemetry); + }) as typeof Telemetry.sendTelemetryEvent); + setSharedPropertyStub = sinon.stub(Telemetry, 'setSharedProperty'); + + eventProperties = new Map<string, string>(); + eventProperties.set('foo', 'one'); + eventProperties.set('bar', 'two'); + + experimentTelemetry = new ExperimentationTelemetry(); + }); + + teardown(() => { + telemetryEvents = []; + sinon.restore(); + }); + + test('Calling postEvent should send a telemetry event', () => { + experimentTelemetry.postEvent(event, eventProperties); + + sinon.assert.calledOnce(sendTelemetryEventStub); + assert.strictEqual(telemetryEvents.length, 1); + assert.deepEqual(telemetryEvents[0], { + eventName: event, + properties: { + foo: 'one', + bar: 'two', + }, + }); + }); + + test('Shared properties should be set for all telemetry events', () => { + const shared = { key: 'shared', value: 'three' }; + + experimentTelemetry.setSharedProperty(shared.key, shared.value); + + sinon.assert.calledOnce(setSharedPropertyStub); + }); +}); diff --git a/src/test/common/extensions.unit.test.ts b/src/test/common/extensions.unit.test.ts 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 aa09bcaf184e..000000000000 --- a/src/test/common/featureDeprecationManager.unit.test.ts +++ /dev/null @@ -1,136 +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 2be773f275fe..d8f82cdbc8e7 100644 --- a/src/test/common/helpers.test.ts +++ b/src/test/common/helpers.test.ts @@ -6,17 +6,15 @@ import { isNotInstalledError } from '../../client/common/helpers'; // Defines a Mocha test suite to group tests of similar kind together suite('helpers', () => { - test('isNotInstalledError', done => { + 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/installer.test.ts b/src/test/common/installer.test.ts deleted file mode 100644 index 34bac00b6ca4..000000000000 --- a/src/test/common/installer.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { InstallationChannelManager } from '../../client/common/installer/channelManager'; -import { ProductInstaller } from '../../client/common/installer/productInstaller'; -import { CTagsProductPathService, 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 { Logger } from '../../client/common/logger'; -import { PersistentStateFactory } from '../../client/common/persistentState'; -import { PathUtils } from '../../client/common/platform/pathUtils'; -import { CurrentProcess } from '../../client/common/process/currentProcess'; -import { IProcessServiceFactory } from '../../client/common/process/types'; -import { IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IsWindows, ModuleNamePurpose, Product, ProductType } from '../../client/common/types'; -import { createDeferred } from '../../client/common/utils/async'; -import { getNamesAndValues } from '../../client/common/utils/enum'; -import { rootWorkspaceUri, updateSetting } from '../common'; -import { MockModuleInstaller } from '../mocks/moduleInstaller'; -import { MockProcessService } from '../mocks/proc'; -import { UnitTestIocContainer } from '../unittests/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.serviceManager.addSingleton<IPersistentStateFactory>(IPersistentStateFactory, PersistentStateFactory); - ioc.serviceManager.addSingleton<ILogger>(ILogger, Logger); - ioc.serviceManager.addSingleton<IInstaller>(IInstaller, ProductInstaller); - ioc.serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils); - 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.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); - } - 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); - if (args.length > 1 && args[0] === '-c' && args[1] === `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)); - if (prod.value === Product.ctags || prod.value === Product.unittest || prod.value === Product.isort) { - 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)); - if (prod.value === Product.unittest || prod.value === Product.ctags || prod.value === Product.isort) { - return; - } - await testInstallingProduct(prod.value); - }); - }); -}); diff --git a/src/test/common/installer/channelManager.unit.test.ts b/src/test/common/installer/channelManager.unit.test.ts new file mode 100644 index 000000000000..9789f9f18718 --- /dev/null +++ b/src/test/common/installer/channelManager.unit.test.ts @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { InstallationChannelManager } from '../../../client/common/installer/channelManager'; +import { IModuleInstaller } from '../../../client/common/installer/types'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { Product } from '../../../client/common/types'; +import { Installer } from '../../../client/common/utils/localize'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { EnvironmentType } from '../../../client/pythonEnvironments/info'; + +suite('InstallationChannelManager - getInstallationChannel()', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let appShell: TypeMoq.IMock<IApplicationShell>; + + let getInstallationChannels: sinon.SinonStub<any>; + + let showNoInstallersMessage: sinon.SinonStub<any>; + const resource = Uri.parse('a'); + let installChannelManager: InstallationChannelManager; + + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + serviceContainer.setup((s) => s.get<IApplicationShell>(IApplicationShell)).returns(() => appShell.object); + }); + + teardown(() => { + sinon.restore(); + }); + + test('If there is exactly one installation channel, return it', async () => { + const moduleInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller.setup((m) => m.name).returns(() => 'singleChannel'); + moduleInstaller.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); + + const channel = await installChannelManager.getInstallationChannel(undefined as any, resource); + expect(channel).to.not.equal(undefined, 'Channel should be set'); + expect(channel!.name).to.equal('singleChannel'); + }); + + test('If no channels are returned by the resource, show no installer message and return', async () => { + getInstallationChannels = sinon.stub(InstallationChannelManager.prototype, 'getInstallationChannels'); + getInstallationChannels.resolves([]); + showNoInstallersMessage = sinon.stub(InstallationChannelManager.prototype, 'showNoInstallersMessage'); + showNoInstallersMessage.resolves(); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); + expect(channel).to.equal(undefined, 'should be undefined'); + assert.ok(showNoInstallersMessage.calledOnceWith(resource)); + }); + + test('If no channel is selected in the quickpick, return undefined', async () => { + const moduleInstaller1 = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller1.setup((m) => m.displayName).returns(() => 'moduleInstaller1'); + moduleInstaller1.setup((m) => (m as any).then).returns(() => undefined); + const moduleInstaller2 = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller2.setup((m) => m.displayName).returns(() => 'moduleInstaller2'); + moduleInstaller2.setup((m) => (m as any).then).returns(() => undefined); + appShell + .setup((a) => a.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + getInstallationChannels = sinon.stub(InstallationChannelManager.prototype, 'getInstallationChannels'); + getInstallationChannels.resolves([moduleInstaller1.object, moduleInstaller2.object]); + showNoInstallersMessage = sinon.stub(InstallationChannelManager.prototype, 'showNoInstallersMessage'); + showNoInstallersMessage.resolves(); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); + assert.ok(showNoInstallersMessage.notCalled); + appShell.verifyAll(); + expect(channel).to.equal(undefined, 'Channel should not be set'); + }); + + test('If multiple channels are returned by the resource, show quick pick of the channel names and return the selected channel installer', async () => { + const moduleInstaller1 = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller1.setup((m) => m.displayName).returns(() => 'moduleInstaller1'); + moduleInstaller1.setup((m) => (m as any).then).returns(() => undefined); + const moduleInstaller2 = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller2.setup((m) => m.displayName).returns(() => 'moduleInstaller2'); + moduleInstaller2.setup((m) => (m as any).then).returns(() => undefined); + const selection = { + label: 'some label', + description: '', + installer: moduleInstaller2.object, + }; + appShell + .setup((a) => a.showQuickPick<typeof selection>(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(selection)) + .verifiable(TypeMoq.Times.once()); + getInstallationChannels = sinon.stub(InstallationChannelManager.prototype, 'getInstallationChannels'); + getInstallationChannels.resolves([moduleInstaller1.object, moduleInstaller2.object]); + showNoInstallersMessage = sinon.stub(InstallationChannelManager.prototype, 'showNoInstallersMessage'); + showNoInstallersMessage.resolves(); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + + const channel = await installChannelManager.getInstallationChannel(Product.pytest, resource); + assert.ok(showNoInstallersMessage.notCalled); + appShell.verifyAll(); + expect(channel).to.not.equal(undefined, 'Channel should be set'); + expect(channel!.displayName).to.equal('moduleInstaller2'); + }); +}); + +suite('InstallationChannelManager - getInstallationChannels()', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + const resource = Uri.parse('a'); + let installChannelManager: InstallationChannelManager; + + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + }); + + test('If no installers are returned by serviceContainer, return an empty list', async () => { + serviceContainer.setup((s) => s.getAll<IModuleInstaller>(IModuleInstaller)).returns(() => []); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + const channel = await installChannelManager.getInstallationChannels(resource); + assert.deepEqual(channel, []); + }); + + test('Return highest priority supported installers', async () => { + const moduleInstallers: IModuleInstaller[] = []; + // Setup 2 installers with priority 1, where one is supported and other is not + for (let i = 0; i < 2; i = i + 1) { + const moduleInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller.setup((m) => (m as any).then).returns(() => undefined); + moduleInstaller.setup((m) => m.priority).returns(() => 1); + moduleInstaller.setup((m) => m.isSupported(resource)).returns(() => Promise.resolve(i % 2 === 0)); + moduleInstallers.push(moduleInstaller.object); + } + // Setup 3 installers with priority 2, where two are supported and other is not + for (let i = 2; i < 5; i = i + 1) { + const moduleInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller.setup((m) => (m as any).then).returns(() => undefined); + moduleInstaller.setup((m) => m.priority).returns(() => 2); + moduleInstaller.setup((m) => m.isSupported(resource)).returns(() => Promise.resolve(i % 2 === 0)); + moduleInstallers.push(moduleInstaller.object); + } + // Setup 2 installers with priority 3, but none are supported + for (let i = 5; i < 7; i = i + 1) { + const moduleInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + moduleInstaller.setup((m) => (m as any).then).returns(() => undefined); + moduleInstaller.setup((m) => m.priority).returns(() => 3); + moduleInstaller.setup((m) => m.isSupported(resource)).returns(() => Promise.resolve(false)); + moduleInstallers.push(moduleInstaller.object); + } + serviceContainer.setup((s) => s.getAll<IModuleInstaller>(IModuleInstaller)).returns(() => moduleInstallers); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + const channels = await installChannelManager.getInstallationChannels(resource); + // Verify that highest supported priority is 2, so number of installers supported with that priority is 2 + expect(channels.length).to.equal(2); + for (let i = 0; i < 2; i = i + 1) { + expect(channels[i].priority).to.equal(2); + } + }); +}); + +suite('InstallationChannelManager - showNoInstallersMessage()', () => { + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + const resource = Uri.parse('a'); + let installChannelManager: InstallationChannelManager; + + setup(() => { + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + }); + + test('If no active interpreter is returned, simply return', async () => { + serviceContainer + .setup((s) => s.get<IInterpreterService>(IInterpreterService)) + .returns(() => interpreterService.object); + serviceContainer.setup((s) => s.get<IApplicationShell>(IApplicationShell)).verifiable(TypeMoq.Times.never()); + interpreterService.setup((i) => i.getActiveInterpreter(resource)).returns(() => Promise.resolve(undefined)); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + await installChannelManager.showNoInstallersMessage(resource); + serviceContainer.verifyAll(); + }); + + test('If active interpreter is Conda, show conda prompt', async () => { + const activeInterpreter = { + envType: EnvironmentType.Conda, + }; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + serviceContainer + .setup((s) => s.get<IInterpreterService>(IInterpreterService)) + .returns(() => interpreterService.object); + serviceContainer + .setup((s) => s.get<IApplicationShell>(IApplicationShell)) + .returns(() => appShell.object) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((i) => i.getActiveInterpreter(resource)) + + .returns(() => Promise.resolve(activeInterpreter as any)); + appShell + .setup((a) => a.showErrorMessage(Installer.noCondaOrPipInstaller, Installer.searchForHelp)) + .verifiable(TypeMoq.Times.once()); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + await installChannelManager.showNoInstallersMessage(resource); + serviceContainer.verifyAll(); + appShell.verifyAll(); + }); + + test('If active interpreter is not Conda, show pip prompt', async () => { + const activeInterpreter = { + envType: EnvironmentType.Pipenv, + }; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + serviceContainer + .setup((s) => s.get<IInterpreterService>(IInterpreterService)) + .returns(() => interpreterService.object); + serviceContainer + .setup((s) => s.get<IApplicationShell>(IApplicationShell)) + .returns(() => appShell.object) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((i) => i.getActiveInterpreter(resource)) + + .returns(() => Promise.resolve(activeInterpreter as any)); + appShell + .setup((a) => a.showErrorMessage(Installer.noPipInstaller, Installer.searchForHelp)) + .verifiable(TypeMoq.Times.once()); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + await installChannelManager.showNoInstallersMessage(resource); + serviceContainer.verifyAll(); + appShell.verifyAll(); + }); + + [EnvironmentType.Conda, EnvironmentType.Pipenv].forEach((interpreterType) => { + [ + { + osName: 'Windows', + isWindows: true, + isMac: false, + }, + { + osName: 'Linux', + isWindows: false, + isMac: false, + }, + { + osName: 'MacOS', + isWindows: false, + isMac: true, + }, + ].forEach((testParams) => { + const expectedURL = `https://www.bing.com/search?q=Install Pip ${testParams.osName} ${ + interpreterType === EnvironmentType.Conda ? 'Conda' : '' + }`; + test(`If \'Search for help\' is selected in error prompt, open correct URL for ${ + testParams.osName + } when Interpreter type is ${ + interpreterType === EnvironmentType.Conda ? 'Conda' : 'not Conda' + }`, async () => { + const activeInterpreter = { + envType: interpreterType, + }; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + const platformService = TypeMoq.Mock.ofType<IPlatformService>(); + serviceContainer + .setup((s) => s.get<IInterpreterService>(IInterpreterService)) + .returns(() => interpreterService.object); + serviceContainer + .setup((s) => s.get<IApplicationShell>(IApplicationShell)) + .returns(() => appShell.object) + .verifiable(TypeMoq.Times.once()); + serviceContainer + .setup((s) => s.get<IPlatformService>(IPlatformService)) + .returns(() => platformService.object) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((i) => i.getActiveInterpreter(resource)) + + .returns(() => Promise.resolve(activeInterpreter as any)); + platformService.setup((p) => p.isWindows).returns(() => testParams.isWindows); + platformService.setup((p) => p.isMac).returns(() => testParams.isMac); + appShell + .setup((a) => a.showErrorMessage(TypeMoq.It.isAny(), Installer.searchForHelp)) + .returns(() => Promise.resolve(Installer.searchForHelp)) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.openUrl(expectedURL)) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + await installChannelManager.showNoInstallersMessage(resource); + serviceContainer.verifyAll(); + appShell.verifyAll(); + }); + }); + }); + test("If 'Search for help' is not selected in error prompt, don't open URL", async () => { + const activeInterpreter = { + envType: EnvironmentType.Conda, + }; + const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + const platformService = TypeMoq.Mock.ofType<IPlatformService>(); + serviceContainer + .setup((s) => s.get<IInterpreterService>(IInterpreterService)) + .returns(() => interpreterService.object); + serviceContainer + .setup((s) => s.get<IApplicationShell>(IApplicationShell)) + .returns(() => appShell.object) + .verifiable(TypeMoq.Times.once()); + serviceContainer + .setup((s) => s.get<IPlatformService>(IPlatformService)) + .returns(() => platformService.object) + .verifiable(TypeMoq.Times.never()); + interpreterService + .setup((i) => i.getActiveInterpreter(resource)) + + .returns(() => Promise.resolve(activeInterpreter as any)); + platformService.setup((p) => p.isWindows).returns(() => true); + appShell + .setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString(), Installer.searchForHelp)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.openUrl(TypeMoq.It.isAny())) + .returns(() => undefined) + .verifiable(TypeMoq.Times.never()); + installChannelManager = new InstallationChannelManager(serviceContainer.object); + await installChannelManager.showNoInstallersMessage(resource); + serviceContainer.verifyAll(); + appShell.verifyAll(); + }); +}); diff --git a/src/test/common/installer/condaInstaller.unit.test.ts b/src/test/common/installer/condaInstaller.unit.test.ts new file mode 100644 index 000000000000..64a4a35539e4 --- /dev/null +++ b/src/test/common/installer/condaInstaller.unit.test.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { instance, mock, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { CondaInstaller } from '../../../client/common/installer/condaInstaller'; +import { InterpreterUri } from '../../../client/common/installer/types'; +import { ExecutionInfo, IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { ICondaService, IComponentAdapter } from '../../../client/interpreter/contracts'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { CondaEnvironmentInfo } from '../../../client/pythonEnvironments/common/environmentManagers/conda'; +import { CondaService } from '../../../client/pythonEnvironments/common/environmentManagers/condaService'; + +suite('Common - Conda Installer', () => { + let installer: CondaInstallerTest; + let serviceContainer: IServiceContainer; + let condaService: ICondaService; + let condaLocatorService: IComponentAdapter; + let configService: IConfigurationService; + class CondaInstallerTest extends CondaInstaller { + public async getExecutionInfo(moduleName: string, resource?: InterpreterUri): Promise<ExecutionInfo> { + return super.getExecutionInfo(moduleName, resource); + } + } + 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.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); + installer._isCondaAvailable = false; + + const supported = await installer.isSupported(uri); + + assert.strictEqual(supported, false); + }); + test('Installer is not supported when conda is not available', async () => { + const uri = Uri.file(__filename); + when(condaService.isCondaAvailable()).thenResolve(false); + + const supported = await installer.isSupported(uri); + + assert.strictEqual(supported, false); + }); + test('Installer is not supported when current env is not a conda env', async () => { + const uri = Uri.file(__filename); + const settings: IPythonSettings = mock(PythonSettings); + const pythonPath = 'my py path'; + + when(settings.pythonPath).thenReturn(pythonPath); + when(condaService.isCondaAvailable()).thenResolve(true); + when(configService.getSettings(uri)).thenReturn(instance(settings)); + when(condaLocatorService.isCondaEnvironment(pythonPath)).thenResolve(false); + + const supported = await installer.isSupported(uri); + + assert.strictEqual(supported, false); + }); + test('Installer is supported when current env is a conda env', async () => { + const uri = Uri.file(__filename); + const settings: IPythonSettings = mock(PythonSettings); + const pythonPath = 'my py path'; + + when(settings.pythonPath).thenReturn(pythonPath); + when(condaService.isCondaAvailable()).thenResolve(true); + when(configService.getSettings(uri)).thenReturn(instance(settings)); + when(condaLocatorService.isCondaEnvironment(pythonPath)).thenResolve(true); + + const supported = await installer.isSupported(uri); + + assert.strictEqual(supported, true); + }); + test('Include name of environment', async () => { + const uri = Uri.file(__filename); + const settings: IPythonSettings = mock(PythonSettings); + const pythonPath = 'my py path'; + const condaPath = 'some Conda Path'; + const condaEnv: CondaEnvironmentInfo = { + name: 'Hello', + path: 'Some Path', + }; + + when(configService.getSettings(uri)).thenReturn(instance(settings)); + when(settings.pythonPath).thenReturn(pythonPath); + when(condaService.getCondaFile(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, + useShell: true, + }); + }); + test('Include path of environment', async () => { + const uri = Uri.file(__filename); + const settings: IPythonSettings = mock(PythonSettings); + const pythonPath = 'my py path'; + const condaPath = 'some Conda Path'; + const condaEnv: CondaEnvironmentInfo = { + name: '', + path: 'Some Path', + }; + + when(configService.getSettings(uri)).thenReturn(instance(settings)); + when(settings.pythonPath).thenReturn(pythonPath); + when(condaService.getCondaFile(true)).thenResolve(condaPath); + when(condaLocatorService.getCondaEnvironment(pythonPath)).thenResolve(condaEnv); + + const execInfo = await installer.getExecutionInfo('abc', uri); + + assert.deepEqual(execInfo, { + args: ['install', '--prefix', condaEnv.path.fileToCommandArgumentForPythonExt(), 'abc', '-y'], + execPath: condaPath, + useShell: true, + }); + }); +}); 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 65fc3cb40765..000000000000 --- a/src/test/common/installer/installer.invalidPath.unit.test.ts +++ /dev/null @@ -1,96 +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 { IServiceContainer } from '../../../client/ioc/types'; - -use(chaiAsPromised); - -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); - - 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: { - 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(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 5ce43dcab3cf..000000000000 --- a/src/test/common/installer/installer.unit.test.ts +++ /dev/null @@ -1,345 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// tslint:disable:max-func-body-length no-invalid-this - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -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'; -// tslint:disable-next-line:ordered-imports -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../../client/common/application/types'; -import { WorkspaceService } from '../../../client/common/application/workspace'; -import { ConfigurationService } from '../../../client/common/configuration/service'; -// tslint:disable-next-line:ordered-imports -import { Commands } from '../../../client/common/constants'; -import '../../../client/common/extensions'; -import { 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 { - IConfigurationService, IDisposableRegistry, ILogger, 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 { ServiceContainer } from '../../../client/ioc/container'; -import { IServiceContainer } from '../../../client/ioc/types'; - -use(chaiAsPromised); - -suite('Module Installer only', () => { - [undefined, Uri.file('resource')].forEach(resource => { - getNamesAndValues<Product>(Product).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>; - const productService = new ProductService(); - - setup(() => { - promptDeferred = createDeferred<string>(); - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - const 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)); - - const 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); - - 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(); - } - }); - }); - - switch (product.value) { - case Product.isort: - case Product.ctags: { - 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); - }); - } - default: { - 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); - const logger = TypeMoq.Mock.ofType<ILogger>(); - logger.setup(l => l.logError(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => new Error('UnitTesting')); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILogger), TypeMoq.It.isAny())).returns(() => logger.object); - - moduleInstaller.setup(m => m.installModule(TypeMoq.It.isValue(moduleName), TypeMoq.It.isValue(resource))).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.Times.once()); - } - }); - 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); - const logger = TypeMoq.Mock.ofType<ILogger>(); - logger.setup(l => l.logError(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => new Error('UnitTesting')); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILogger), TypeMoq.It.isAny())).returns(() => logger.object); - - moduleInstaller.setup(m => m.installModule(TypeMoq.It.isValue(moduleName), TypeMoq.It.isValue(resource))).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.Times.once()); - } - }); - if (product.value !== Product.unittest) { - 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(); - - // Display a few more prompts. - installer.promptToInstall(product.value, resource).ignoreErrors(); - installer.promptToInstall(product.value, resource).ignoreErrors(); - installer.promptToInstall(product.value, resource).ignoreErrors(); - installer.promptToInstall(product.value, resource).ignoreErrors(); - - 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 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(); - }); - } - } - } - }); - - 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); - }); - - 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)!; - // tslint:disable-next-line:no-any - when(appShell.showErrorMessage(`Linter ${productName} is not installed.`, 'Install', options[0], options[1])).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); - }); - }); - }); -}); diff --git a/src/test/common/installer/moduleInstaller.unit.test.ts b/src/test/common/installer/moduleInstaller.unit.test.ts index c98df67839c5..3df64ceb2dec 100644 --- a/src/test/common/installer/moduleInstaller.unit.test.ts +++ b/src/test/common/installer/moduleInstaller.unit.test.ts @@ -1,32 +1,55 @@ +/* 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'; +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 { Disposable, OutputChannel, Uri, WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../../client/common/application/types'; +import { CancellationTokenSource, Disposable, ProgressLocation, Uri, WorkspaceConfiguration } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; 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 } 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 { IConfigurationService, IDisposableRegistry, IPythonSettings, ModuleNamePurpose, Product } from '../../../client/common/types'; +import { + ExecutionInfo, + IConfigurationService, + IDisposableRegistry, + IInstaller, + ILogOutputChannel, + IPythonSettings, + Product, +} from '../../../client/common/types'; import { getNamesAndValues } from '../../../client/common/utils/enum'; import { noop } from '../../../client/common/utils/misc'; -import { ICondaService, IInterpreterService, InterpreterType, PythonInterpreter } 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 * as logging from '../../../client/logging'; +import { EnvironmentType, ModuleInstallerType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +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. -Both approachs have their benefits. +Both approaches have their benefits. -Comnbinations of: +Combinations of: 1. With and without a workspace. 2. Http Proxy configuration. 3. All products. @@ -36,206 +59,528 @@ Comnbinations of: 7. All installers. */ suite('Module Installer', () => { - const pythonPath = path.join(__dirname, 'python'); - [CondaInstaller, PipInstaller, PipEnvInstaller].forEach(installerClass => { + class TestModuleInstaller extends ModuleInstaller { + public get priority(): number { + return 0; + } + + public get name(): string { + return ''; + } + + public get displayName(): string { + return ''; + } + + public get type(): ModuleInstallerType { + return ModuleInstallerType.Unknown; + } + + public isSupported(): Promise<boolean> { + return Promise.resolve(false); + } + + public getExecutionInfo(): Promise<ExecutionInfo> { + return Promise.resolve({ moduleName: 'executionInfo', args: [] }); + } + + public elevatedInstall(execPath: string, args: string[]) { + return super.elevatedInstall(execPath, args); + } + } + let outputChannel: TypeMoq.IMock<ILogOutputChannel>; + + let appShell: TypeMoq.IMock<IApplicationShell>; + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + + 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<ILogOutputChannel>(); + serviceContainer + .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); + installer = new TestModuleInstaller(serviceContainer.object); + }); + teardown(() => { + rewiremock.disable(); + sinon.restore(); + }); + + test('Show error message if sudo exec fails with error', async () => { + const error = 'Error message'; + const sudoPromptMock = { + // 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); + appShell + .setup((a) => a.showErrorMessage(error)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + installer.elevatedInstall(execPath, args); + appShell.verifyAll(); + traceLogStub.calledOnceWithExactly(`[Elevated] ${command}`); + }); + + test('Show stdout if sudo exec succeeds', async () => { + const stdout = 'stdout'; + const sudoPromptMock = { + // 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); + outputChannel + .setup((o) => o.show()) + .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 = { + // 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 + .setup((o) => o.show()) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + installer.elevatedInstall(execPath, args); + traceLogStub.calledOnceWithExactly(`[Elevated] ${command}`); + traceLogStub.calledOnceWithExactly(`Warning: ${stderr}`); + }); + }); + + [CondaInstaller, PipInstaller, PipEnvInstaller, TestModuleInstaller].forEach((InstallerClass) => { // Proxy info is relevant only for PipInstaller. - const proxyServers = installerClass === PipInstaller ? ['', 'proxy:1234'] : ['']; - proxyServers.forEach(proxyServer => { - [undefined, Uri.file('/users/dev/xyz')].forEach(resource => { + const proxyServers = InstallerClass === PipInstaller ? ['', 'proxy:1234'] : ['']; + proxyServers.forEach((proxyServer) => { + [undefined, Uri.file('/users/dev/xyz')].forEach((resource) => { // Conda info is relevant only for CondaInstaller. - const condaEnvs = installerClass === CondaInstaller ? [ - { name: 'My-Env01', path: '' }, { name: '', path: path.join('conda', 'path') }, - { name: 'My-Env01 With Spaces', path: '' }, { name: '', path: path.join('conda with spaces', 'path') } - ] : []; - [undefined, ...condaEnvs].forEach(condaEnvInfo => { + const condaEnvs = + InstallerClass === CondaInstaller + ? [ + { name: 'My-Env01', path: '' }, + { name: '', path: path.join('conda', 'path') }, + { name: 'My-Env01 With Spaces', path: '' }, + { name: '', path: path.join('conda with spaces', 'path') }, + ] + : []; + [undefined, ...condaEnvs].forEach((condaEnvInfo) => { const testProxySuffix = proxyServer.length === 0 ? 'without proxy info' : 'with proxy info'; - const testCondaEnv = condaEnvInfo ? (condaEnvInfo.name ? 'without conda name' : 'with conda path') : 'without conda'; - const testSuite = [testProxySuffix, testCondaEnv].filter(item => item.length > 0).join(', '); - suite(`${installerClass.name} (${testSuite})`, () => { + // 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})`, () => { let disposables: Disposable[] = []; - let installer: IModuleInstaller; let installationChannel: TypeMoq.IMock<IInstallationChannelManager>; - let serviceContainer: TypeMoq.IMock<IServiceContainer>; let terminalService: TypeMoq.IMock<ITerminalService>; + let configService: TypeMoq.IMock<IConfigurationService>; + let fs: TypeMoq.IMock<IFileSystem>; let pythonSettings: TypeMoq.IMock<IPythonSettings>; let interpreterService: TypeMoq.IMock<IInterpreterService>; + let installer: IModuleInstaller; const condaExecutable = 'my.exe'; setup(() => { serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) + .returns(() => appShell.object); + + fs = TypeMoq.Mock.ofType<IFileSystem>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))) + .returns(() => fs.object); + disposables = []; - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())).returns(() => disposables); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) + .returns(() => disposables); installationChannel = TypeMoq.Mock.ofType<IInstallationChannelManager>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInstallationChannelManager), TypeMoq.It.isAny())).returns(() => installationChannel.object); + serviceContainer + .setup((c) => + c.get(TypeMoq.It.isValue(IInstallationChannelManager), TypeMoq.It.isAny()), + ) + .returns(() => installationChannel.object); const condaService = TypeMoq.Mock.ofType<ICondaService>(); - condaService.setup(c => c.getCondaFile()).returns(() => Promise.resolve(condaExecutable)); - condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(condaEnvInfo)); + 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)); - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => configService.object); + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) + .returns(() => configService.object); pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + pythonSettings.setup((p) => p.pythonPath).returns(() => pythonPath); + configService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns(() => pythonSettings.object); terminalService = TypeMoq.Mock.ofType<ITerminalService>(); const terminalServiceFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>(); - terminalServiceFactory.setup(f => f.getTerminalService(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => terminalService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory), TypeMoq.It.isAny())).returns(() => terminalServiceFactory.object); + terminalServiceFactory + .setup((f) => f.getTerminalService(TypeMoq.It.isAny())) + .returns(() => terminalService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalServiceFactory), TypeMoq.It.isAny())) + .returns(() => terminalServiceFactory.object); interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())).returns(() => interpreterService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICondaService), TypeMoq.It.isAny())).returns(() => condaService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) + .returns(() => interpreterService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ICondaService), TypeMoq.It.isAny())) + .returns(() => condaService.object); const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())).returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) + .returns(() => workspaceService.object); const http = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - http.setup(h => h.get(TypeMoq.It.isValue('proxy'), TypeMoq.It.isAny())).returns(() => proxyServer); - workspaceService.setup(w => w.getConfiguration(TypeMoq.It.isValue('http'))).returns(() => http.object); - - installer = new installerClass(serviceContainer.object); + http.setup((h) => h.get(TypeMoq.It.isValue('proxy'), TypeMoq.It.isAny())).returns( + () => proxyServer, + ); + workspaceService + .setup((w) => w.getConfiguration(TypeMoq.It.isValue('http'))) + .returns(() => http.object); + installer = new InstallerClass(serviceContainer.object); }); teardown(() => { - disposables.forEach(disposable => { + disposables.forEach((disposable) => { if (disposable) { disposable.dispose(); } }); + 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))) - .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 = ['-m', '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"'); - 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 = ['-m', 'pip', ...proxyArgs, 'install', '-U', 'pylint']; - await installModuleAndVerifyCommand(pythonPath, expectedArgs); - }); + 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(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(); + }); + }); + } + + 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, + ); } - if (installerClass === PipEnvInstaller) { - test(testTitle, async () => { - setActiveInterpreter(interpreterInfo); - const expectedArgs = ['install', 'pylint', '--dev']; - await installModuleAndVerifyCommand(pipenvName, expectedArgs); - }); + setActiveInterpreter(); + try { + await installer.installModule(product.value, resource); + } catch { + noop(); } - 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'); - await installModuleAndVerifyCommand(condaExecutable, expectedArgs); - }); + interpreterService.verifyAll(); + }); + 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(); + }); + 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')]; + + await installModuleAndVerifyCommand(pythonPath, expectedArgs); + interpreterService.verifyAll(); + }); } - }); - return; - } - - if (installerClass === PipInstaller) { - test(`Ensure getActiveInterperter 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 = ['-m', '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']; - 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); - await installModuleAndVerifyCommand(condaExecutable, expectedArgs); - }); - } - }); + } + 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, + ); + }); + }); + } + 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, + ); + }); + }); + } + }); }); }); }); @@ -243,30 +588,18 @@ 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 => { + .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 }[]; + .filter((item) => item !== undefined) as { name: string; value: Product; moduleName: string }[]; } diff --git a/src/test/common/installer/pipEnvInstaller.unit.test.ts b/src/test/common/installer/pipEnvInstaller.unit.test.ts new file mode 100644 index 000000000000..25b1b910daaa --- /dev/null +++ b/src/test/common/installer/pipEnvInstaller.unit.test.ts @@ -0,0 +1,113 @@ +// 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 { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { PipEnvInstaller } from '../../../client/common/installer/pipEnvInstaller'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; +import * as pipEnvHelper from '../../../client/pythonEnvironments/common/environmentManagers/pipenv'; +import { EnvironmentType } from '../../../client/pythonEnvironments/info'; + +suite('PipEnv installer', async () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + 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>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + serviceContainer + .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'); + }); + + test('Installer priority is 10', () => { + expect(pipEnvInstaller.priority).to.equal(10); + }); + + test('If InterpreterUri is Pipenv interpreter, method isSupported() returns true', async () => { + const interpreter = { + envType: EnvironmentType.Pipenv, + }; + + const result = await pipEnvInstaller.isSupported(interpreter as any); + expect(result).to.equal(true, 'Should be true'); + }); + + test('If InterpreterUri is Python interpreter but not of type Pipenv, method isSupported() returns false', async () => { + const interpreter = { + envType: EnvironmentType.Conda, + }; + + const result = await pipEnvInstaller.isSupported(interpreter as any); + expect(result).to.equal(false, 'Should be false'); + }); + + test('If active environment is pipenv and is related to workspace folder, return true', async () => { + const resource = Uri.parse('a'); + + 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 active environment is not pipenv, return false', async () => { + const resource = Uri.parse('a'); + 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 new file mode 100644 index 000000000000..7b7af714f7f7 --- /dev/null +++ b/src/test/common/installer/pipInstaller.unit.test.ts @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { PipInstaller } from '../../../client/common/installer/pipInstaller'; +import { IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +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); + pipInstaller = new PipInstaller(serviceContainer.object); + }); + + test('Installer name is Pip', () => { + expect(pipInstaller.name).to.equal('Pip'); + }); + + test('Installer priority is 0', () => { + expect(pipInstaller.priority).to.equal(0); + }); + + test('If InterpreterUri is Python interpreter, Python execution factory is called with the correct arguments', async () => { + const pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + pythonExecutionFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .callback((options) => { + assert.deepEqual(options, { resource: undefined, pythonPath: interpreter.path }); + }) + .returns(() => Promise.resolve(pythonExecutionService.object)) + .verifiable(TypeMoq.Times.once()); + pythonExecutionService.setup((p) => (p as any).then).returns(() => undefined); + + await pipInstaller.isSupported(interpreter as any); + + pythonExecutionFactory.verifyAll(); + }); + + test('If InterpreterUri is Resource, Python execution factory is called with the correct arguments', async () => { + const pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + const resource = Uri.parse('a'); + pythonExecutionFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .callback((options) => { + assert.deepEqual(options, { resource, pythonPath: undefined }); + }) + .returns(() => Promise.resolve(pythonExecutionService.object)) + .verifiable(TypeMoq.Times.once()); + pythonExecutionService.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.setup((p) => (p as any).then).returns(() => undefined); + pythonExecutionService.setup((p) => p.isModuleInstalled('pip')).returns(() => Promise.resolve(true)); + + const expected = await pipInstaller.isSupported(resource); + + expect(expected).to.equal(true, 'Should be true'); + }); + + test('Method isSupported() returns false if pip module is not installed', async () => { + const pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + const resource = Uri.parse('a'); + pythonExecutionFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonExecutionService.object)); + pythonExecutionService.setup((p) => (p as any).then).returns(() => undefined); + pythonExecutionService.setup((p) => p.isModuleInstalled('pip')).returns(() => Promise.resolve(false)); + + const expected = await pipInstaller.isSupported(resource); + + expect(expected).to.equal(false, 'Should be false'); + }); + + test('Method isSupported() returns false if checking if pip module is installed fails with error', async () => { + const pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + const resource = Uri.parse('a'); + pythonExecutionFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonExecutionService.object)); + pythonExecutionService.setup((p) => (p as any).then).returns(() => undefined); + pythonExecutionService + .setup((p) => p.isModuleInstalled('pip')) + .returns(() => Promise.reject('Unable to check if module is installed')); + + const expected = await pipInstaller.isSupported(resource); + + expect(expected).to.equal(false, 'Should be false'); + }); +}); diff --git a/src/test/common/installer/poetryInstaller.unit.test.ts b/src/test/common/installer/poetryInstaller.unit.test.ts new file mode 100644 index 000000000000..07d60159138e --- /dev/null +++ b/src/test/common/installer/poetryInstaller.unit.test.ts @@ -0,0 +1,191 @@ +// 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 * as assert from 'assert'; +import { expect } from 'chai'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { PoetryInstaller } from '../../../client/common/installer/poetryInstaller'; +import { 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'; + +suite('Module Installer - Poetry', () => { + class TestInstaller extends PoetryInstaller { + 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 interpreterService: IInterpreterService; + let serviceContainer: ServiceContainer; + let shellExecute: sinon.SinonStub; + + setup(() => { + serviceContainer = mock(ServiceContainer); + interpreterService = mock<IInterpreterService>(); + when(serviceContainer.get<IInterpreterService>(IInterpreterService)).thenReturn(instance(interpreterService)); + workspaceService = mock(WorkspaceService); + configurationService = mock(ConfigurationService); + + 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), + ); + }); + + teardown(() => { + shellExecute?.restore(); + }); + + test('Installer name is poetry', () => { + expect(poetryInstaller.name).to.equal('poetry'); + }); + + test('Installer priority is 10', () => { + expect(poetryInstaller.priority).to.equal(10); + }); + + test('Installer display name is poetry', () => { + expect(poetryInstaller.displayName).to.equal('poetry'); + }); + + test('Is not supported when there is no resource', async () => { + const supported = await poetryInstaller.isSupported(); + assert.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.strictEqual(supported, false); + }); + test('Get Executable info', async () => { + const uri = Uri.file(__dirname); + const settings = mock(PythonSettings); + + when(configurationService.getSettings(uri)).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry path'); + + const info = await poetryInstaller.getExecutionInfo('something', uri); + + assert.deepEqual(info, { args: ['add', '--group', 'dev', 'something'], execPath: 'poetry path' }); + }); + test('Get executable info when installing black', async () => { + const uri = Uri.file(__dirname); + const settings = mock(PythonSettings); + + when(configurationService.getSettings(uri)).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry path'); + + const info = await poetryInstaller.getExecutionInfo('black', uri); + + assert.deepEqual(info, { + args: ['add', '--group', 'dev', 'black'], + execPath: 'poetry path', + }); + }); + 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 }); + + const supported = await poetryInstaller.isSupported(Uri.file(__filename)); + + assert.strictEqual(supported, true); + }); + + test('Is supported returns true if no interpreter is selected', async () => { + const uri = Uri.file(project1); + const settings = mock(PythonSettings); + + 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 }); + + const supported = await poetryInstaller.isSupported(Uri.file(__filename)); + + assert.strictEqual(supported, false); + }); + + 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(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 supported = await poetryInstaller.isSupported(Uri.file(__filename)); + + assert.strictEqual(supported, false); + }); + + test('Is supported returns false if selected interpreter is not of Poetry type', 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.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 supported = await poetryInstaller.isSupported(Uri.file(__filename)); + + 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 739360b5b0fa..000000000000 --- a/src/test/common/installer/productPath.unit.test.ts +++ /dev/null @@ -1,189 +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 { CTagsProductPathService, 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, IUnitTestSettings, 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/unittests/common/types'; - -use(chaiAsPromised); - -suite('Product Path', () => { - [undefined, Uri.file('resource')].forEach(resource => { - getNamesAndValues<Product>(Product).forEach(product => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let formattingSettings: TypeMoq.IMock<IFormattingSettings>; - let unitTestSettings: TypeMoq.IMock<IUnitTestSettings>; - 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<IUnitTestSettings>(); - 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.unitTest).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; - } - 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(); - }); - } - 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; - } - default: { - test(`No tests for Product Path of this Product Type ${product.name}`, () => { - fail('No tests for Product Path of this Product Type'); - }); - } - } - }); - }); -}); diff --git a/src/test/common/installer/serviceRegistry.unit.test.ts b/src/test/common/installer/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..8a811ad7ac4d --- /dev/null +++ b/src/test/common/installer/serviceRegistry.unit.test.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { InstallationChannelManager } from '../../../client/common/installer/channelManager'; +import { CondaInstaller } from '../../../client/common/installer/condaInstaller'; +import { PipEnvInstaller } from '../../../client/common/installer/pipEnvInstaller'; +import { PipInstaller } from '../../../client/common/installer/pipInstaller'; +import { PoetryInstaller } from '../../../client/common/installer/poetryInstaller'; +import { TestFrameworkProductPathService } from '../../../client/common/installer/productPath'; +import { ProductService } from '../../../client/common/installer/productService'; +import { registerTypes } from '../../../client/common/installer/serviceRegistry'; +import { + IInstallationChannelManager, + IModuleInstaller, + IProductPathService, + IProductService, +} from '../../../client/common/installer/types'; +import { ProductType } from '../../../client/common/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; + +suite('Common installer Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager)); + verify(serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, CondaInstaller)).once(); + verify(serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipInstaller)).once(); + verify(serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PipEnvInstaller)).once(); + verify(serviceManager.addSingleton<IModuleInstaller>(IModuleInstaller, PoetryInstaller)).once(); + verify( + serviceManager.addSingleton<IInstallationChannelManager>( + IInstallationChannelManager, + InstallationChannelManager, + ), + ).once(); + verify(serviceManager.addSingleton<IProductService>(IProductService, ProductService)).once(); + verify( + serviceManager.addSingleton<IProductPathService>( + IProductPathService, + TestFrameworkProductPathService, + ProductType.TestFramework, + ), + ).once(); + }); +}); diff --git a/src/test/common/interpreterPathService.unit.test.ts b/src/test/common/interpreterPathService.unit.test.ts new file mode 100644 index 000000000000..58a34b3cbcde --- /dev/null +++ b/src/test/common/interpreterPathService.unit.test.ts @@ -0,0 +1,503 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { + ConfigurationChangeEvent, + ConfigurationTarget, + Event, + EventEmitter, + Uri, + WorkspaceConfiguration, +} from 'vscode'; +import { IApplicationEnvironment, IWorkspaceService } from '../../client/common/application/types'; +import { + defaultInterpreterPathSetting, + getCIPythonPath, + InterpreterPathService, +} from '../../client/common/interpreterPathService'; +import { FileSystemPaths } from '../../client/common/platform/fs-paths'; +import { InterpreterConfigurationScope, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; +import { createDeferred, sleep } from '../../client/common/utils/async'; + +suite('Interpreter Path Service', async () => { + let interpreterPathService: InterpreterPathService; + let persistentStateFactory: TypeMoq.IMock<IPersistentStateFactory>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let appEnvironment: TypeMoq.IMock<IApplicationEnvironment>; + const resource = Uri.parse('a'); + const resourceOutsideOfWorkspace = Uri.parse('b'); + const interpreterPath = 'path/to/interpreter'; + const fs = FileSystemPaths.withDefaults(); + setup(() => { + const event = TypeMoq.Mock.ofType<Event<ConfigurationChangeEvent>>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + 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, + })); + 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, + [], + appEnvironment.object, + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Global settings are not updated if stored value is same as new value', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceConfig + .setup((w) => w.inspect<string>('defaultInterpreterPath')) + .returns( + () => + ({ + globalValue: interpreterPath, + } as any), + ); + workspaceConfig + .setup((w) => w.update('defaultInterpreterPath', interpreterPath, true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await interpreterPathService.update(resource, ConfigurationTarget.Global, interpreterPath); + + workspaceConfig.verifyAll(); + }); + + test('Global settings are correctly updated otherwise', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => workspaceConfig.object); + workspaceConfig + .setup((w) => w.inspect<string>('defaultInterpreterPath')) + .returns( + () => + ({ + globalValue: 'storedValue', + } as any), + ); + workspaceConfig + .setup((w) => w.update('defaultInterpreterPath', interpreterPath, true)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.Global, interpreterPath); + + workspaceConfig.verifyAll(); + }); + + test('Workspace settings are not updated if stored value is same as new value', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.once()); + persistentState.setup((p) => p.value).returns(() => interpreterPath); + persistentState + .setup((p) => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await interpreterPathService.update(resource, ConfigurationTarget.Workspace, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Workspace settings are correctly updated if a folder is directly opened', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.once()); + persistentState + .setup((p) => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.Workspace, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Ensure the correct event is fired if Workspace settings are updated', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object); + persistentState.setup((p) => p.updateValue(interpreterPath)).returns(() => Promise.resolve()); + + const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType<EventEmitter<InterpreterConfigurationScope>>(); + interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; + _didChangeInterpreterEmitter + .setup((emitter) => emitter.fire({ uri: resource, configTarget: ConfigurationTarget.Workspace })) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.Workspace, interpreterPath); + + _didChangeInterpreterEmitter.verifyAll(); + }); + + test('Workspace settings are correctly updated in case of multiroot folders', async () => { + const workspaceFileUri = Uri.parse('path/to/workspaceFile'); + const expectedSettingKey = `WORKSPACE_INTERPRETER_PATH_${fs.normCase(workspaceFileUri.fsPath)}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.workspaceFile).returns(() => workspaceFileUri); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.once()); + persistentState + .setup((p) => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.Workspace, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Workspace folder settings are correctly updated in case of multiroot folders', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.once()); + persistentState + .setup((p) => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Ensure the correct event is fired if Workspace folder settings are updated', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.once()); + persistentState + .setup((p) => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType<EventEmitter<InterpreterConfigurationScope>>(); + interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; + _didChangeInterpreterEmitter + .setup((emitter) => emitter.fire({ uri: resource, configTarget: ConfigurationTarget.WorkspaceFolder })) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + + await interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, interpreterPath); + + _didChangeInterpreterEmitter.verifyAll(); + }); + + test('Updating workspace settings simply returns if no workspace is opened', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.never()); + persistentState + .setup((p) => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await interpreterPathService.update(resourceOutsideOfWorkspace, ConfigurationTarget.Workspace, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Updating workspace folder settings simply returns if no workspace is opened', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.never()); + persistentState + .setup((p) => p.updateValue(interpreterPath)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await interpreterPathService.update(resourceOutsideOfWorkspace, ConfigurationTarget.Workspace, interpreterPath); + + persistentState.verifyAll(); + persistentStateFactory.verifyAll(); + }); + + test('Inspecting settings returns as expected if no workspace is opened', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + workspaceService + .setup((w) => w.getConfiguration('python', TypeMoq.It.isAny())) + .returns(() => workspaceConfig.object); + workspaceConfig + .setup((w) => w.inspect<string>('defaultInterpreterPath')) + .returns( + () => + ({ + globalValue: 'default/path/to/interpreter', + } as any), + ); + const persistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => persistentState.object) + .verifiable(TypeMoq.Times.never()); + + const settings = interpreterPathService.inspect(resourceOutsideOfWorkspace); + assert.deepEqual(settings, { + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: undefined, + workspaceValue: undefined, + }); + + persistentStateFactory.verifyAll(); + }); + + test('Inspecting settings returns as expected if a folder is directly opened', async () => { + const expectedSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + // No workspace file is present if a folder is directly opened + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.getConfiguration('python', resource)).returns(() => workspaceConfig.object); + workspaceConfig + .setup((w) => w.inspect<string>('defaultInterpreterPath')) + .returns( + () => + ({ + globalValue: 'default/path/to/interpreter', + } as any), + ); + const workspaceFolderPersistentState = TypeMoq.Mock.ofType<IPersistentState<string | undefined>>(); + workspaceService.setup((w) => w.workspaceFolders).returns(() => undefined); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => workspaceFolderPersistentState.object); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedSettingKey, undefined)) + .returns(() => workspaceFolderPersistentState.object); + workspaceFolderPersistentState.setup((p) => p.value).returns(() => 'workspaceFolderValue'); + + const settings = interpreterPathService.inspect(resource); + + assert.deepEqual(settings, { + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: 'workspaceFolderValue', + workspaceValue: 'workspaceFolderValue', + }); + }); + + test('Inspecting settings returns as expected in case of multiroot folders', async () => { + const workspaceFileUri = Uri.parse('path/to/workspaceFile'); + const expectedWorkspaceSettingKey = `WORKSPACE_INTERPRETER_PATH_${fs.normCase(workspaceFileUri.fsPath)}`; + const expectedWorkspaceFolderSettingKey = `WORKSPACE_FOLDER_INTERPRETER_PATH_${resource.fsPath}`; + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + // A workspace file is present in case of multiroot workspace folders + workspaceService.setup((w) => w.workspaceFile).returns(() => workspaceFileUri); + workspaceService.setup((w) => w.getWorkspaceFolderIdentifier(resource)).returns(() => resource.fsPath); + workspaceService.setup((w) => w.getConfiguration('python', resource)).returns(() => workspaceConfig.object); + workspaceConfig + .setup((w) => w.inspect<string>('defaultInterpreterPath')) + .returns( + () => + ({ + 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), + ) + .returns(() => workspaceFolderPersistentState.object); + persistentStateFactory + .setup((p) => p.createGlobalPersistentState<string | undefined>(expectedWorkspaceSettingKey, undefined)) + .returns(() => workspacePersistentState.object); + workspaceFolderPersistentState.setup((p) => p.value).returns(() => 'workspaceFolderValue'); + workspacePersistentState.setup((p) => p.value).returns(() => 'workspaceValue'); + + const settings = interpreterPathService.inspect(resource); + + assert.deepEqual(settings, { + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: 'workspaceFolderValue', + workspaceValue: 'workspaceValue', + }); + }); + + test('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: 'defaultWorkspaceFolderValue', + workspaceValue: 'defaultWorkspaceValue', + }); + }); + + test(`Getting setting value returns workspace folder value if it's defined`, async () => { + interpreterPathService.inspect = () => ({ + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: 'workspaceFolderValue', + workspaceValue: 'workspaceValue', + }); + const settingValue = interpreterPathService.get(resource); + expect(settingValue).to.equal('workspaceFolderValue'); + }); + + test(`Getting setting value returns workspace value if workspace folder value is 'undefined'`, async () => { + interpreterPathService.inspect = () => ({ + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: undefined, + workspaceValue: 'workspaceValue', + }); + const settingValue = interpreterPathService.get(resource); + expect(settingValue).to.equal('workspaceValue'); + }); + + test(`Getting setting value returns global value if workspace folder & workspace value are 'undefined'`, async () => { + interpreterPathService.inspect = () => ({ + globalValue: 'default/path/to/interpreter', + workspaceFolderValue: undefined, + workspaceValue: undefined, + }); + const settingValue = interpreterPathService.get(resource); + expect(settingValue).to.equal('default/path/to/interpreter'); + }); + + test(`Getting setting value returns 'python' if all workspace folder, workspace, and global value are 'undefined'`, async () => { + interpreterPathService.inspect = () => ({ + globalValue: undefined, + workspaceFolderValue: undefined, + workspaceValue: undefined, + }); + const settingValue = interpreterPathService.get(resource); + + expect(settingValue).to.equal(getCIPythonPath()); + }); + + test('If defaultInterpreterPathSetting is changed, an event is fired', async () => { + const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType<EventEmitter<InterpreterConfigurationScope>>(); + const event = TypeMoq.Mock.ofType<ConfigurationChangeEvent>(); + event + .setup((e) => e.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; + _didChangeInterpreterEmitter + .setup((emitter) => emitter.fire({ uri: undefined, configTarget: ConfigurationTarget.Global })) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + await interpreterPathService.onDidChangeConfiguration(event.object); + _didChangeInterpreterEmitter.verifyAll(); + event.verifyAll(); + }); + + test('If some other setting changed, no event is fired', async () => { + const _didChangeInterpreterEmitter = TypeMoq.Mock.ofType<EventEmitter<InterpreterConfigurationScope>>(); + const event = TypeMoq.Mock.ofType<ConfigurationChangeEvent>(); + event + .setup((e) => e.affectsConfiguration(`python.${defaultInterpreterPathSetting}`)) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterPathService._didChangeInterpreterEmitter = _didChangeInterpreterEmitter.object; + _didChangeInterpreterEmitter + .setup((emitter) => emitter.fire(TypeMoq.It.isAny())) + .returns(() => undefined) + .verifiable(TypeMoq.Times.never()); + await interpreterPathService.onDidChangeConfiguration(event.object); + _didChangeInterpreterEmitter.verifyAll(); + event.verifyAll(); + }); + + test('Ensure on interpreter change captures the fired event with the correct arguments', async () => { + const deferred = createDeferred<true>(); + const interpreterConfigurationScope = { uri: undefined, configTarget: ConfigurationTarget.Global }; + interpreterPathService.onDidChange((i) => { + expect(i).to.equal(interpreterConfigurationScope); + deferred.resolve(true); + }); + interpreterPathService._didChangeInterpreterEmitter.fire(interpreterConfigurationScope); + const eventCaptured = await Promise.race([deferred.promise, sleep(1000).then(() => false)]); + expect(eventCaptured).to.equal(true, 'Event should be captured'); + }); +}); diff --git a/src/test/common/localize.unit.test.ts b/src/test/common/localize.unit.test.ts deleted file mode 100644 index e3f512249d55..000000000000 --- a/src/test/common/localize.unit.test.ts +++ /dev/null @@ -1,81 +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'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import * as localize from '../../client/common/utils/localize'; - -// Defines a Mocha test suite to group tests of similar kind together -suite('localize tests', () => { - - test('keys', done => { - const val = localize.LanguageService.bannerMessage(); - assert.equal(val, 'Can you please take 2 minutes to tell us how the Python Language Server is working for you?', 'LanguageService string doesnt match'); - done(); - }); - - test('keys italian', done => { - // Force a config change - process.env.VSCODE_NLS_CONFIG = '{ "locale": "it" }'; - - const val = localize.LanguageService.bannerLabelYes(); - assert.equal(val, 'Sì, prenderò il sondaggio ora', 'bannerLabelYes is not being translated'); - done(); - }); - - test('keys exist', done => { - // Read all of the namespaces from the localize import - const entries = Object.keys(localize); - - // Read in the JSON object for the package.nls.json - let nlsCollection = {}; - const defaultNlsFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json'); - if (fs.existsSync(defaultNlsFile)) { - const contents = fs.readFileSync(defaultNlsFile, 'utf8'); - nlsCollection = JSON.parse(contents); - } else { - nlsCollection = {}; - } - - // Now match all of our namespace entries to our nls entries - entries.forEach((e : string) => { - if (typeof localize[e] !== 'function') { - // This must be a namespace. It should have functions inside of it - const namespace = localize[e]; - const funcs = Object.keys(namespace); - - // Run every function, this should fill up our asked for keys collection - funcs.forEach((f : string) => { - const func = namespace[f]; - func(); - }); - } - }); - - // Now verify all of the asked for keys exist - const askedFor = localize.getAskedForCollection(); - const missing = {}; - 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(); - }); -}); diff --git a/src/test/common/misc.test.ts b/src/test/common/misc.test.ts index 59e426217a4a..370668d40e7e 100644 --- a/src/test/common/misc.test.ts +++ b/src/test/common/misc.test.ts @@ -8,7 +8,7 @@ import { isTestExecution } from '../../client/common/constants'; // Defines a Mocha test suite to group tests of similar kind together suite('Common - Misc', () => { - test('Ensure its identified that we\'re running unit tests', () => { + test("Ensure its identified that we're running unit tests", () => { expect(isTestExecution()).to.be.equal(true, 'incorrect'); }); }); diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index 1aee5db08f7c..0cdb6f270c54 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -1,102 +1,176 @@ -// tslint:disable:max-func-body-length - -import { expect } from 'chai'; -import * as path from 'path'; +import { expect, should as chaiShould, use as chaiUse } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; import { SemVer } from 'semver'; +import { instance, mock } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; +import { Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; +import { ActiveResourceService } from '../../client/common/application/activeResource'; +import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { ClipboardService } from '../../client/common/application/clipboard'; +import { CommandManager } from '../../client/common/application/commandManager'; +import { ReloadVSCodeCommandHandler } from '../../client/common/application/commands/reloadCommand'; +import { ReportIssueCommandHandler } from '../../client/common/application/commands/reportIssueCommand'; +import { DebugService } from '../../client/common/application/debugService'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { Extensions } from '../../client/common/application/extensions'; +import { + IActiveResourceService, + IApplicationEnvironment, + IApplicationShell, + IClipboard, + ICommandManager, + IDebugService, + IDocumentManager, + IJupyterExtensionDependencyManager, + IWorkspaceService, +} from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; import { ConfigurationService } from '../../client/common/configuration/service'; +import { ExperimentService } from '../../client/common/experiments/service'; import { CondaInstaller } from '../../client/common/installer/condaInstaller'; import { PipEnvInstaller } from '../../client/common/installer/pipEnvInstaller'; import { PipInstaller } from '../../client/common/installer/pipInstaller'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; import { IModuleInstaller } from '../../client/common/installer/types'; -import { Logger } from '../../client/common/logger'; +import { InterpreterPathService } from '../../client/common/interpreterPathService'; +import { BrowserService } from '../../client/common/net/browser'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { FileSystem } from '../../client/common/platform/fileSystem'; import { PathUtils } from '../../client/common/platform/pathUtils'; import { PlatformService } from '../../client/common/platform/platformService'; import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; import { CurrentProcess } from '../../client/common/process/currentProcess'; -import { IProcessServiceFactory, IPythonExecutionFactory } from '../../client/common/process/types'; -import { ITerminalService, ITerminalServiceFactory } from '../../client/common/terminal/types'; -import { IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IPythonSettings, IsWindows } from '../../client/common/types'; +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 { 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'; +import { TerminalHelper } from '../../client/common/terminal/helper'; +import { SettingsShellDetector } from '../../client/common/terminal/shellDetectors/settingsShellDetector'; +import { TerminalNameShellDetector } from '../../client/common/terminal/shellDetectors/terminalNameShellDetector'; +import { UserEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; +import { VSCEnvironmentShellDetector } from '../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; +import { + IShellDetector, + ITerminalActivationCommandProvider, + ITerminalActivationHandler, + ITerminalActivator, + ITerminalHelper, + ITerminalService, + ITerminalServiceFactory, + TerminalActivationProviders, +} from '../../client/common/terminal/types'; +import { + IBrowserService, + IConfigurationService, + ICurrentProcess, + IExperimentService, + IExtensions, + IInstaller, + IInterpreterPathService, + IPathUtils, + IPersistentStateFactory, + IPythonSettings, + IRandom, + IsWindows, +} from '../../client/common/types'; +import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; import { Architecture } from '../../client/common/utils/platform'; -import { ICondaService, IInterpreterLocatorService, IInterpreterService, INTERPRETER_LOCATOR_SERVICE, InterpreterType, PIPENV_SERVICE, PythonInterpreter } from '../../client/interpreter/contracts'; +import { Random } from '../../client/common/utils/random'; +import { + ICondaService, + IInterpreterService, + IComponentAdapter, + IActivatedEnvironmentLaunch, +} from '../../client/interpreter/contracts'; import { IServiceContainer } from '../../client/ioc/types'; -import { getExtensionSettings, PYTHON_PATH, rootWorkspaceUri } from '../common'; +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 { PYTHON_PATH } from '../common'; import { MockModuleInstaller } from '../mocks/moduleInstaller'; import { MockProcessService } from '../mocks/proc'; -import { UnitTestIocContainer } from '../unittests/serviceRegistry'; -import { closeActiveWindows, initializeTest } from './../initialize'; +import { UnitTestIocContainer } from '../testing/serviceRegistry'; +import { closeActiveWindows, initializeTest } from '../initialize'; +import { createTypeMoq } from '../mocks/helper'; + +chaiUse(chaiAsPromised.default); -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', () => { - [undefined, Uri.file(__filename)].forEach(resource => { + [undefined, Uri.file(__filename)].forEach((resource) => { 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 () => { - initializeDI(); + chaiShould(); + await initializeDI(); await initializeTest(); - await resetSettings(); }); suiteTeardown(async () => { await closeActiveWindows(); - await resetSettings(); }); teardown(async () => { await ioc.dispose(); 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<ILogger>(ILogger, Logger); + 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)) + mockTerminalFactory + .setup((t) => t.getTerminalService(TypeMoq.It.isAny())) + .callback((passedInResource) => expect(passedInResource).to.be.deep.equal({ resource })) .returns(() => mockTerminalService.object); - ioc.serviceManager.addSingletonInstance<ITerminalServiceFactory>(ITerminalServiceFactory, mockTerminalFactory.object); - + ioc.serviceManager.addSingletonInstance<ITerminalServiceFactory>( + ITerminalServiceFactory, + 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); - condaService = TypeMoq.Mock.ofType<ICondaService>(); - ioc.serviceManager.addSingletonInstance<ICondaService>(ICondaService, condaService.object); - - interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); - ioc.serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, interpreterService.object); ioc.serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils); ioc.serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess); @@ -104,37 +178,96 @@ suite('Module Installer', () => { ioc.serviceManager.addSingleton<IPlatformService>(IPlatformService, PlatformService); ioc.serviceManager.addSingleton<IConfigurationService>(IConfigurationService, ConfigurationService); - const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - ioc.serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, workspaceService.object); - const http = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - http.setup(h => h.get(TypeMoq.It.isValue('proxy'), TypeMoq.It.isAny())).returns(() => ''); - workspaceService.setup(w => w.getConfiguration(TypeMoq.It.isValue('http'))).returns(() => http.object); + ioc.serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, new WorkspaceService()); ioc.registerMockProcessTypes(); ioc.serviceManager.addSingletonInstance<boolean>(IsWindows, false); - } - async function resetSettings(): Promise<void> { - const configService = ioc.serviceManager.get<IConfigurationService>(IConfigurationService); - await configService.updateSetting('linting.pylintEnabled', true, rootWorkspaceUri, ConfigurationTarget.Workspace); - } - async function getCurrentPythonPath(): Promise<string> { - const pythonPath = getExtensionSettings(workspaceUri).pythonPath; - if (path.basename(pythonPath) === pythonPath) { - const pythonProc = await ioc.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create({ resource: workspaceUri }); - return pythonProc.getExecutablePath().catch(() => pythonPath); - } else { - return pythonPath; - } + + await ioc.registerMockInterpreterTypes(); + condaService = createTypeMoq<ICondaService>(); + condaLocatorService = createTypeMoq<IComponentAdapter>(); + ioc.serviceManager.rebindInstance<ICondaService>(ICondaService, condaService.object); + interpreterService = createTypeMoq<IInterpreterService>(); + ioc.serviceManager.rebindInstance<IInterpreterService>(IInterpreterService, interpreterService.object); + + ioc.serviceManager.addSingleton<IActiveResourceService>(IActiveResourceService, ActiveResourceService); + ioc.serviceManager.addSingleton<IInterpreterPathService>(IInterpreterPathService, InterpreterPathService); + ioc.serviceManager.addSingleton<IExtensions>(IExtensions, Extensions); + ioc.serviceManager.addSingleton<IRandom>(IRandom, Random); + ioc.serviceManager.addSingleton<IApplicationShell>(IApplicationShell, ApplicationShell); + ioc.serviceManager.addSingleton<IClipboard>(IClipboard, ClipboardService); + ioc.serviceManager.addSingleton<ICommandManager>(ICommandManager, CommandManager); + ioc.serviceManager.addSingleton<IDocumentManager>(IDocumentManager, DocumentManager); + ioc.serviceManager.addSingleton<IDebugService>(IDebugService, DebugService); + ioc.serviceManager.addSingleton<IApplicationEnvironment>(IApplicationEnvironment, ApplicationEnvironment); + ioc.serviceManager.addSingleton<IJupyterExtensionDependencyManager>( + IJupyterExtensionDependencyManager, + JupyterExtensionDependencyManager, + ); + ioc.serviceManager.addSingleton<IBrowserService>(IBrowserService, BrowserService); + ioc.serviceManager.addSingleton<ITerminalActivator>(ITerminalActivator, TerminalActivator); + ioc.serviceManager.addSingleton<ITerminalActivationHandler>( + ITerminalActivationHandler, + PowershellTerminalActivationFailedHandler, + ); + ioc.serviceManager.addSingleton<IExperimentService>(IExperimentService, ExperimentService); + + ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + Bash, + TerminalActivationProviders.bashCShellFish, + ); + ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + CommandPromptAndPowerShell, + TerminalActivationProviders.commandPromptAndPowerShell, + ); + ioc.serviceManager.addSingleton<ITerminalActivationCommandProvider>( + ITerminalActivationCommandProvider, + Nushell, + TerminalActivationProviders.nushell, + ); + 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<IMultiStepInputFactory>(IMultiStepInputFactory, MultiStepInputFactory); + ioc.serviceManager.addSingleton<IImportTracker>(IImportTracker, ImportTracker); + ioc.serviceManager.addBinding(IImportTracker, IExtensionSingleActivationService); + 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<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + ReloadVSCodeCommandHandler, + ); + ioc.serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + ReportIssueCommandHandler, + ); } 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.addSingletonInstance<IInterpreterLocatorService>(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); - ioc.serviceManager.addSingletonInstance<IInterpreterLocatorService>(IInterpreterLocatorService, TypeMoq.Mock.ofType<IInterpreterLocatorService>().object, PIPENV_SERVICE); - - const processService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create() as MockProcessService; - processService.onExec((file, args, options, callback) => { + ioc.serviceManager.addSingletonInstance<IModuleInstaller>( + IModuleInstaller, + new MockModuleInstaller('mock', true), + ); + ioc.serviceManager.addSingletonInstance<ITerminalHelper>(ITerminalHelper, instance(mock(TerminalHelper))); + 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: '' }); } @@ -145,31 +278,30 @@ suite('Module Installer', () => { const moduleInstallers = ioc.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller); expect(moduleInstallers).length(4, 'Incorrect number of installers'); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); await expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); - const condaInstaller = moduleInstallers.find(item => item.displayName === 'Conda')!; + const condaInstaller = moduleInstallers.find((item) => item.displayName === 'Conda')!; expect(condaInstaller).not.to.be.an('undefined', 'Conda installer not found'); await expect(condaInstaller.isSupported()).to.eventually.equal(false, 'Conda is supported'); - const mockInstaller = moduleInstallers.find(item => item.displayName === 'mock')!; + const mockInstaller = moduleInstallers.find((item) => item.displayName === 'mock')!; expect(mockInstaller).not.to.be.an('undefined', 'mock installer not found'); await expect(mockInstaller.isSupported()).to.eventually.equal(true, 'mock is not supported'); }); test('Ensure pip is supported', async () => { - ioc.serviceManager.addSingletonInstance<IModuleInstaller>(IModuleInstaller, new MockModuleInstaller('mock', true)); - const pythonPath = await getCurrentPythonPath(); - const mockInterpreterLocator = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); - mockInterpreterLocator.setup(p => p.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve([ - { ...info, architecture: Architecture.Unknown, companyDisplayName: '', displayName: '', envName: '', path: pythonPath, type: InterpreterType.Conda, version: new SemVer('1.0.0') } - ])); - ioc.serviceManager.addSingletonInstance<IInterpreterLocatorService>(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); - ioc.serviceManager.addSingletonInstance<IInterpreterLocatorService>(IInterpreterLocatorService, TypeMoq.Mock.ofType<IInterpreterLocatorService>().object, PIPENV_SERVICE); - - const processService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create() as MockProcessService; - processService.onExec((file, args, options, callback) => { + ioc.serviceManager.addSingletonInstance<IModuleInstaller>( + IModuleInstaller, + new MockModuleInstaller('mock', true), + ); + ioc.serviceManager.addSingletonInstance<ITerminalHelper>(ITerminalHelper, instance(mock(TerminalHelper))); + + const processService = (await ioc.serviceContainer + .get<IProcessServiceFactory>(IProcessServiceFactory) + .create()) as MockProcessService; + processService.onExec((file, args, _options, callback) => { if (args.length > 1 && args[0] === '-c' && args[1] === 'import pip') { callback({ stdout: '' }); } @@ -180,38 +312,55 @@ suite('Module Installer', () => { const moduleInstallers = ioc.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller); expect(moduleInstallers).length(4, 'Incorrect number of installers'); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); await expect(pipInstaller.isSupported()).to.eventually.equal(true, 'Pip is not supported'); }); test('Ensure conda is supported', async () => { - const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + const serviceContainer = createTypeMoq<IServiceContainer>(); - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); - const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + const configService = createTypeMoq<IConfigurationService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + 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); - condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(true)); - condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(true)); + 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)); + condaLocatorService + .setup((c) => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)); const condaInstaller = new CondaInstaller(serviceContainer.object); await expect(condaInstaller.isSupported()).to.eventually.equal(true, 'Conda is not supported'); }); test('Ensure conda is not supported even if conda is available', async () => { - const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + const serviceContainer = createTypeMoq<IServiceContainer>(); - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); - const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + const configService = createTypeMoq<IConfigurationService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + 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); - condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(true)); - condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(false)); + 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)); + condaLocatorService + .setup((c) => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(false)); const condaInstaller = new CondaInstaller(serviceContainer.object); await expect(condaInstaller.isSupported()).to.eventually.equal(false, 'Conda should not be supported'); @@ -219,90 +368,94 @@ 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.addSingletonInstance<IInterpreterLocatorService>(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); - ioc.serviceManager.addSingletonInstance<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())).returns(() => Promise.resolve(interpreter)); + interpreterService + .setup((x) => x.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(interpreter)); const moduleName = 'xyz'; const moduleInstallers = ioc.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); let argsSent: string[] = []; mockTerminalService - .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) - .returns((cmd: string, args: string[]) => { argsSent = args; return Promise.resolve(void 0); }); - // tslint:disable-next-line:no-any - interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())).returns(() => Promise.resolve({ type: InterpreterType.Unknown } as any)); + .setup((t) => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((_cmd: string, args: string[]) => { + argsSent = args; + return Promise.resolve(); + }); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + + // 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(`-m pip install -U ${moduleName} --user`, 'Invalid command sent to terminal for installation.'); + expect(argsSent.join(' ')).equal( + `-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.addSingletonInstance<IInterpreterLocatorService>(IInterpreterLocatorService, mockInterpreterLocator.object, INTERPRETER_LOCATOR_SERVICE); - ioc.serviceManager.addSingletonInstance<IInterpreterLocatorService>(IInterpreterLocatorService, TypeMoq.Mock.ofType<IInterpreterLocatorService>().object, PIPENV_SERVICE); - const moduleName = 'xyz'; const moduleInstallers = ioc.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'Pip')!; expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); let argsSent: string[] = []; mockTerminalService - .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) - .returns((cmd: string, args: string[]) => { argsSent = args; return Promise.resolve(void 0); }); + .setup((t) => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((_cmd: string, args: string[]) => { + argsSent = args; + return Promise.resolve(); + }); await pipInstaller.installModule(moduleName, resource); mockTerminalFactory.verifyAll(); - expect(argsSent.join(' ')).equal(`-m pip install -U ${moduleName}`, 'Invalid command sent to terminal for installation.'); + expect(argsSent.join(' ')).equal( + `-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.addSingletonInstance<IInterpreterLocatorService>(IInterpreterLocatorService, mockInterpreterLocator.object, PIPENV_SERVICE); - const moduleName = 'xyz'; const moduleInstallers = ioc.serviceContainer.getAll<IModuleInstaller>(IModuleInstaller); - const pipInstaller = moduleInstallers.find(item => item.displayName === 'pipenv')!; + const pipInstaller = moduleInstallers.find((item) => item.displayName === 'pipenv')!; expect(pipInstaller).not.to.be.an('undefined', 'pipenv installer not found'); let argsSent: string[] = []; let command: string | undefined; mockTerminalService - .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) + .setup((t) => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns((cmd: string, args: string[]) => { argsSent = args; command = cmd; - return Promise.resolve(void 0); + return Promise.resolve(); }); await pipInstaller.installModule(moduleName, resource); mockTerminalFactory.verifyAll(); expect(command!).equal('pipenv', 'Invalid command sent to terminal for installation.'); - expect(argsSent.join(' ')).equal(`install ${moduleName} --dev`, 'Invalid command arguments sent to terminal for installation.'); + expect(argsSent.join(' ')).equal( + `install ${moduleName} --dev`, + 'Invalid command arguments sent to terminal for installation.', + ); }); }); }); 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 e32e906b28aa..000000000000 --- a/src/test/common/net/httpClient.unit.test.ts +++ /dev/null @@ -1,35 +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 { WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../../client/common/application/types'; -import { HttpClient } from '../../../client/common/net/httpClient'; -import { IServiceContainer } from '../../../client/ioc/types'; - -suite('Http Client', () => { - test('Get proxy info', async () => { - const container = TypeMoq.Mock.ofType<IServiceContainer>(); - const workSpaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - const config = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - const proxy = 'https://myproxy.net:4242'; - 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); - - const httpClient = new HttpClient(container.object); - - config.verifyAll(); - workSpaceService.verifyAll(); - expect(httpClient.requestOptions).to.deep.equal({ proxy: proxy }); - }); -}); diff --git a/src/test/common/nuget/azureBobStoreRepository.test.ts b/src/test/common/nuget/azureBobStoreRepository.test.ts deleted file mode 100644 index 90724f3115a8..000000000000 --- a/src/test/common/nuget/azureBobStoreRepository.test.ts +++ /dev/null @@ -1,51 +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 { LanguageServerPackageStorageContainers } from '../../../client/activation/languageServer/languageServerPackageRepository'; -import { LanguageServerPackageService } from '../../../client/activation/languageServer/languageServerPackageService'; -import { IHttpClient } from '../../../client/activation/types'; -import { IApplicationEnvironment } 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 { 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 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); - - 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 = LanguageServerPackageStorageContainers.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 = { languageServerVersion: '0.1.0' }; - const appEnv = typeMoq.Mock.ofType<IApplicationEnvironment>(); - appEnv.setup(e => e.packageJson).returns(() => packageJson); - const lsPackageService = new LanguageServerPackageService(serviceContainer.object, appEnv.object, platformService); - const packageName = lsPackageService.getNugetPackageName(); - const packages = await repo.getPackages(packageName); - - expect(packages).to.be.length.greaterThan(0); - }); -}); 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 7c47bc126ca1..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 { IHttpClient } from '../../../client/activation/types'; -import { NugetRepository } from '../../../client/common/nuget/nugetRepository'; -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 87d1ca9222e4..000000000000 --- a/src/test/common/nuget/nugetService.unit.test.ts +++ /dev/null @@ -1,26 +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 new file mode 100644 index 000000000000..85a822978ef2 --- /dev/null +++ b/src/test/common/platform/errors.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 vscode from 'vscode'; +import { + isFileExistsError, + isFileIsDirError, + isFileNotFoundError, + isNoPermissionsError, + isNotDirError, +} from '../../../client/common/platform/errors'; +import { SystemError } from './utils'; + +suite('FileSystem - errors', () => { + const filename = 'spam'; + + suite('isFileNotFoundError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.FileNotFound(filename), true], + [vscode.FileSystemError.FileExists(filename), false], + [new SystemError('ENOENT', 'stat', '<msg>'), true], + [new SystemError('EEXIST', '???', '<msg>'), false], + [new Error(filename), undefined], + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isFileNotFoundError(err); + + expect(matches).to.equal(expected); + }); + }); + }); + + suite('isFileExistsError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.FileExists(filename), true], + [vscode.FileSystemError.FileNotFound(filename), false], + [new SystemError('EEXIST', '???', '<msg>'), true], + [new SystemError('ENOENT', 'stat', '<msg>'), false], + [new Error(filename), undefined], + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isFileExistsError(err); + + expect(matches).to.equal(expected); + }); + }); + }); + + suite('isFileIsDirError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.FileIsADirectory(filename), true], + [vscode.FileSystemError.FileNotFound(filename), false], + [new SystemError('EISDIR', '???', '<msg>'), true], + [new SystemError('ENOENT', 'stat', '<msg>'), false], + [new Error(filename), undefined], + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isFileIsDirError(err); + + expect(matches).to.equal(expected); + }); + }); + }); + + suite('isNotDirError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.FileNotADirectory(filename), true], + [vscode.FileSystemError.FileNotFound(filename), false], + [new SystemError('ENOTDIR', '???', '<msg>'), true], + [new SystemError('ENOENT', 'stat', '<msg>'), false], + [new Error(filename), undefined], + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isNotDirError(err); + + expect(matches).to.equal(expected); + }); + }); + }); + + suite('isNoPermissionsError', () => { + const tests: [Error, boolean | undefined][] = [ + [vscode.FileSystemError.NoPermissions(filename), true], + [vscode.FileSystemError.FileNotFound(filename), false], + [new SystemError('EACCES', '???', '<msg>'), true], + [new SystemError('ENOENT', 'stat', '<msg>'), false], + [new Error(filename), undefined], + ]; + tests.map(([err, expected]) => { + test(`${err} -> ${expected}`, () => { + const matches = isNoPermissionsError(err); + + expect(matches).to.equal(expected); + }); + }); + }); +}); diff --git a/src/test/common/platform/filesystem.functional.test.ts b/src/test/common/platform/filesystem.functional.test.ts new file mode 100644 index 000000000000..be9a369935f3 --- /dev/null +++ b/src/test/common/platform/filesystem.functional.test.ts @@ -0,0 +1,779 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect, use } from 'chai'; +import { convertStat, FileSystem, FileSystemUtils, RawFileSystem } from '../../../client/common/platform/fileSystem'; +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'; +import { + assertDoesNotExist, + assertFileText, + DOES_NOT_EXIST, + fixPath, + FSFixture, + SUPPORTS_SOCKETS, + SUPPORTS_SYMLINKS, + WINDOWS, +} from './utils'; + +const assertArrays = require('chai-arrays'); +use(require('chai-as-promised')); +use(assertArrays); + +suite('FileSystem - raw', () => { + let fileSystem: RawFileSystem; + let fix: FSFixture; + setup(async () => { + fileSystem = RawFileSystem.withDefaults(); + fix = new FSFixture(); + + await assertDoesNotExist(DOES_NOT_EXIST); + }); + teardown(async () => { + await fix.cleanUp(); + await fix.ensureDeleted(DOES_NOT_EXIST); + }); + + suite('lstat', () => { + test('for symlinks, gives the link info', async function () { + if (!SUPPORTS_SYMLINKS) { + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + const rawStat = await fs.lstat(symlink); + const expected = convertStat(rawStat, FileType.SymbolicLink); + + const stat = await fileSystem.lstat(symlink); + + expect(stat).to.deep.equal(expected); + }); + + test('for normal files, gives the file info', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + // Ideally we would compare to the result of + // fileSystem.stat(). However, we do not have access + // to the VS Code API here. + const rawStat = await fs.lstat(filename); + const expected = convertStat(rawStat, FileType.File); + + const stat = await fileSystem.lstat(filename); + + expect(stat).to.deep.equal(expected); + }); + + test('fails if the file does not exist', async () => { + const promise = fileSystem.lstat(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('chmod (non-Windows)', () => { + suiteSetup(function () { + // On Windows, chmod won't have any effect on the file itself. + if (WINDOWS) { + this.skip(); + } + }); + + async function checkMode(filename: string, expected: number) { + const stat = await fs.stat(filename); + expect(stat.mode & 0o777).to.equal(expected); + } + + test('the file mode gets updated (string)', async () => { + const filename = await fix.createFile('spam.py', '...'); + await fs.chmod(filename, 0o644); + + await fileSystem.chmod(filename, '755'); + + await checkMode(filename, 0o755); + }); + + test('the file mode gets updated (number)', async () => { + const filename = await fix.createFile('spam.py', '...'); + await fs.chmod(filename, 0o644); + + await fileSystem.chmod(filename, 0o755); + + await checkMode(filename, 0o755); + }); + + test('the file mode gets updated for a directory', async () => { + const dirname = await fix.createDirectory('spam'); + await fs.chmod(dirname, 0o755); + + await fileSystem.chmod(dirname, 0o700); + + await checkMode(dirname, 0o700); + }); + + test('nothing happens if the file mode already matches', async () => { + const filename = await fix.createFile('spam.py', '...'); + await fs.chmod(filename, 0o644); + + await fileSystem.chmod(filename, 0o644); + + await checkMode(filename, 0o644); + }); + + test('fails if the file does not exist', async () => { + const promise = fileSystem.chmod(DOES_NOT_EXIST, 0o755); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('appendText', () => { + test('existing file', async () => { + const orig = 'spamspamspam\n\n'; + const dataToAppend = `Some Data\n${new Date().toString()}\nAnd another line`; + const filename = await fix.createFile('spam.txt', orig); + const expected = `${orig}${dataToAppend}`; + + await fileSystem.appendText(filename, dataToAppend); + + const actual = await fs.readFile(filename, { encoding: 'utf8' }); + expect(actual).to.be.equal(expected); + }); + + test('existing empty file', async () => { + const filename = await fix.createFile('spam.txt'); + const dataToAppend = `Some Data\n${new Date().toString()}\nAnd another line`; + const expected = dataToAppend; + + await fileSystem.appendText(filename, dataToAppend); + + const actual = await fs.readFile(filename, { 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, { encoding: 'utf8' }); + expect(actual).to.be.equal('spam'); + }); + + test('fails if not a file', async () => { + const dirname = await fix.createDirectory('spam'); + + const promise = fileSystem.appendText(dirname, 'spam'); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + // non-async + + suite('statSync', () => { + test('for normal files, gives the file info', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + // Ideally we would compare to the result of + // fileSystem.stat(). However, we do not have access + // to the VS Code API here. + const rawStat = await fs.stat(filename); + const expected = convertStat(rawStat, FileType.File); + + const stat = fileSystem.statSync(filename); + + expect(stat).to.deep.equal(expected); + }); + + test('for symlinks, gives the linked info', async function () { + if (!SUPPORTS_SYMLINKS) { + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + const rawStat = await fs.stat(filename); + const expected = convertStat(rawStat, FileType.SymbolicLink | FileType.File); + + const stat = fileSystem.statSync(symlink); + + expect(stat).to.deep.equal(expected); + }); + + test('fails if the file does not exist', async () => { + expect(() => { + fileSystem.statSync(DOES_NOT_EXIST); + }).to.throw(); + }); + }); + + suite('readTextSync', () => { + test('returns contents of a file', async () => { + const expected = '<some text>'; + const filename = await fix.createFile('x/y/z/spam.py', expected); + + const text = fileSystem.readTextSync(filename); + + expect(text).to.be.equal(expected); + }); + + test('always UTF-8', async () => { + const expected = '... 😁 ...'; + const filename = await fix.createFile('x/y/z/spam.py', expected); + + const text = fileSystem.readTextSync(filename); + + expect(text).to.equal(expected); + }); + + test('throws an exception if file does not exist', () => { + expect(() => { + fileSystem.readTextSync(DOES_NOT_EXIST); + }).to.throw(Error); + }); + }); + + suite('createReadStream', () => { + setup(function () { + // 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. + + this.skip(); + }); + + test('returns the correct ReadStream', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const expected = fs.createReadStream(filename); + expected.destroy(); + + const stream = fileSystem.createReadStream(filename); + stream.destroy(); + + expect(stream.path).to.deep.equal(expected.path); + }); + + // Missing tests: + // * creation fails if the file does not exist + // * .read() works as expected + // * .pipe() works as expected + }); + + suite('createWriteStream', () => { + setup(function () { + // 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. + + this.skip(); + }); + + async function writeToStream(filename: string, write: (str: fs.WriteStream) => void) { + const closeDeferred = createDeferred(); + const stream = fileSystem.createWriteStream(filename); + stream.on('close', () => closeDeferred.resolve()); + write(stream); + stream.end(); + stream.close(); + stream.destroy(); + await closeDeferred.promise; + return stream; + } + + test('returns the correct WriteStream', async () => { + const filename = await fix.resolve('x/y/z/spam.py'); + const expected = fs.createWriteStream(filename); + expected.destroy(); + + const stream = await writeToStream(filename, noop); + + expect(stream.path).to.deep.equal(expected.path); + }); + + test('creates the file if missing', async () => { + const filename = await fix.resolve('x/y/z/spam.py'); + await assertDoesNotExist(filename); + const data = 'line1\nline2\n'; + + await writeToStream(filename, (s) => s.write(data)); + + await assertFileText(filename, data); + }); + + test('always UTF-8', async () => { + const filename = await fix.resolve('x/y/z/spam.py'); + const data = '... 😁 ...'; + + await writeToStream(filename, (s) => s.write(data)); + + await assertFileText(filename, data); + }); + + test('overwrites existing file', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const data = 'line1\nline2\n'; + + await writeToStream(filename, (s) => s.write(data)); + + await assertFileText(filename, data); + }); + }); +}); + +suite('FileSystem - utils', () => { + let utils: FileSystemUtils; + let fix: FSFixture; + setup(async () => { + utils = FileSystemUtils.withDefaults(); + fix = new FSFixture(); + + await assertDoesNotExist(DOES_NOT_EXIST); + }); + teardown(async () => { + await fix.cleanUp(); + await fix.ensureDeleted(DOES_NOT_EXIST); + }); + + suite('getFileHash', () => { + // Since getFileHash() relies on timestamps, we have to take + // into account filesystem timestamp resolution. For instance + // on FAT and HFS it is 1 second. + // See: https://nodejs.org/api/fs.html#fs_stat_time_values + + test('Getting hash for a file should return non-empty string', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const hash = await utils.getFileHash(filename); + + expect(hash).to.not.equal(''); + }); + + test('the returned hash is stable', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const hash1 = await utils.getFileHash(filename); + const hash2 = await utils.getFileHash(filename); + await sleep(2_000); // just in case + const hash3 = await utils.getFileHash(filename); + + expect(hash1).to.equal(hash2); + expect(hash1).to.equal(hash3); + expect(hash2).to.equal(hash3); + }); + + test('the returned hash changes with modification', async () => { + const filename = await fix.createFile('x/y/z/spam.py', 'original text'); + + const hash1 = await utils.getFileHash(filename); + await sleep(2_000); // for filesystems with 1s resolution + await fs.writeFile(filename, 'new text'); + const hash2 = await utils.getFileHash(filename); + + expect(hash1).to.not.equal(hash2); + }); + + test('the returned hash is unique', async () => { + const file1 = await fix.createFile('spam.py'); + await sleep(2_000); // for filesystems with 1s resolution + const file2 = await fix.createFile('x/y/z/spam.py'); + await sleep(2_000); // for filesystems with 1s resolution + const file3 = await fix.createFile('eggs.py'); + + const hash1 = await utils.getFileHash(file1); + const hash2 = await utils.getFileHash(file2); + const hash3 = await utils.getFileHash(file3); + + expect(hash1).to.not.equal(hash2); + expect(hash1).to.not.equal(hash3); + expect(hash2).to.not.equal(hash3); + }); + + test('Getting hash for non existent file should throw error', async () => { + const promise = utils.getFileHash(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('search', () => { + test('found matches', async () => { + const pattern = await fix.resolve(`x/y/z/spam.*`); + const expected: string[] = [ + await fix.createFile('x/y/z/spam.py'), + await fix.createFile('x/y/z/spam.pyc'), + await fix.createFile('x/y/z/spam.so'), + await fix.createDirectory('x/y/z/spam.data'), + ]; + // non-matches + await fix.createFile('x/spam.py'); + await fix.createFile('x/y/z/eggs.py'); + await fix.createFile('x/y/z/spam-all.py'); + await fix.createFile('x/y/z/spam'); + await fix.createFile('x/spam.py'); + + let files = await utils.search(pattern); + + // For whatever reason, on Windows "search()" is + // returning filenames with forward slasshes... + files = files.map(fixPath); + expect(files.sort()).to.deep.equal(expected.sort()); + }); + + test('no matches', async () => { + const pattern = await fix.resolve(`x/y/z/spam.*`); + + const files = await utils.search(pattern); + + expect(files).to.deep.equal([]); + }); + }); + + suite('fileExistsSync', () => { + test('want file, got file', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = utils.fileExistsSync(filename); + + expect(exists).to.equal(true); + }); + + test('want file, not file', async () => { + const filename = await fix.createDirectory('x/y/z/spam.py'); + + const exists = utils.fileExistsSync(filename); + + // Note that currently the "file" can be *anything*. It + // doesn't have to be just a regular file. This is the + // way it already worked, so we're keeping it that way + // for now. + expect(exists).to.equal(true); + }); + + test('symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + + const exists = utils.fileExistsSync(symlink); + + // Note that currently the "file" can be *anything*. It + // doesn't have to be just a regular file. This is the + // way it already worked, so we're keeping it that way + // for now. + expect(exists).to.equal(true); + }); + + test('unknown', async function () { + if (!SUPPORTS_SOCKETS) { + this.skip(); + } + const sockFile = await fix.createSocket('x/y/z/ipc.sock'); + + const exists = utils.fileExistsSync(sockFile); + + // Note that currently the "file" can be *anything*. It + // doesn't have to be just a regular file. This is the + // way it already worked, so we're keeping it that way + // for now. + expect(exists).to.equal(true); + }); + }); +}); + +suite('FileSystem', () => { + let fileSystem: FileSystem; + let fix: FSFixture; + setup(async () => { + fileSystem = new FileSystem(); + fix = new FSFixture(); + + await assertDoesNotExist(DOES_NOT_EXIST); + }); + teardown(async () => { + await fix.cleanUp(); + await fix.ensureDeleted(DOES_NOT_EXIST); + }); + + suite('path-related', () => { + const paths = fs.FileSystemPaths.withDefaults(); + const pathUtils = fs.FileSystemPathUtils.withDefaults(paths); + + suite('directorySeparatorChar', () => { + // tested fully in the FileSystemPaths tests. + + test('matches wrapped object', () => { + const expected = paths.sep; + + const sep = fileSystem.directorySeparatorChar; + + expect(sep).to.equal(expected); + }); + }); + + suite('arePathsSame', () => { + // tested fully in the FileSystemPathUtils tests. + + test('matches wrapped object', () => { + const file1 = fixPath('a/b/c/spam.py'); + const file2 = fixPath('a/b/c/Spam.py'); + const expected = pathUtils.arePathsSame(file1, file2); + + const areSame = fileSystem.arePathsSame(file1, file2); + + expect(areSame).to.equal(expected); + }); + }); + }); + + suite('raw', () => { + suite('appendFile', () => { + test('wraps the low-level impl', async () => { + const filename = await fix.createFile('spam.txt'); + const dataToAppend = `Some Data\n${new Date().toString()}\nAnd another line`; + const expected = dataToAppend; + + await fileSystem.appendFile(filename, dataToAppend); + + const actual = await fs.readFile(filename, { encoding: 'utf8' }); + expect(actual).to.be.equal(expected); + }); + }); + + suite('chmod (non-Windows)', () => { + suiteSetup(function () { + // On Windows, chmod won't have any effect on the file itself. + if (WINDOWS) { + this.skip(); + } + }); + + test('wraps the low-level impl', async () => { + const filename = await fix.createFile('spam.py', '...'); + await fs.chmod(filename, 0o644); + + await fileSystem.chmod(filename, '755'); + + const stat = await fs.stat(filename); + expect(stat.mode & 0o777).to.equal(0o755); + }); + }); + + //============================= + // sync methods + + suite('readFileSync', () => { + test('wraps the low-level impl', async () => { + const expected = '<some text>'; + const filename = await fix.createFile('x/y/z/spam.py', expected); + + const text = fileSystem.readFileSync(filename); + + expect(text).to.be.equal(expected); + }); + }); + + suite('createReadStream', () => { + test('wraps the low-level impl', async function () { + // This test seems to randomly fail. + + this.skip(); + + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const expected = fs.createReadStream(filename); + expected.destroy(); + + const stream = fileSystem.createReadStream(filename); + stream.destroy(); + + expect(stream.path).to.deep.equal(expected.path); + }); + }); + + suite('createWriteStream', () => { + test('wraps the low-level impl', async function () { + // This test seems to randomly fail. + + this.skip(); + + const filename = await fix.resolve('x/y/z/spam.py'); + const expected = fs.createWriteStream(filename); + expected.destroy(); + + const stream = fileSystem.createWriteStream(filename); + stream.destroy(); + + expect(stream.path).to.deep.equal(expected.path); + }); + }); + }); + + suite('utils', () => { + suite('getFileHash', () => { + // Since getFileHash() relies on timestamps, we have to take + // into account filesystem timestamp resolution. For instance + // on FAT and HFS it is 1 second. + // See: https://nodejs.org/api/fs.html#fs_stat_time_values + + test('Getting hash for a file should return non-empty string', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const hash = await fileSystem.getFileHash(filename); + + expect(hash).to.not.equal(''); + }); + + test('the returned hash is stable', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const hash1 = await fileSystem.getFileHash(filename); + const hash2 = await fileSystem.getFileHash(filename); + await sleep(2_000); // just in case + const hash3 = await fileSystem.getFileHash(filename); + + expect(hash1).to.equal(hash2); + expect(hash1).to.equal(hash3); + expect(hash2).to.equal(hash3); + }); + + test('the returned hash changes with modification', async () => { + const filename = await fix.createFile('x/y/z/spam.py', 'original text'); + + const hash1 = await fileSystem.getFileHash(filename); + await sleep(2_000); // for filesystems with 1s resolution + await fs.writeFile(filename, 'new text'); + const hash2 = await fileSystem.getFileHash(filename); + + expect(hash1).to.not.equal(hash2); + }); + + test('the returned hash is unique', async () => { + const file1 = await fix.createFile('spam.py'); + await sleep(2_000); // for filesystems with 1s resolution + const file2 = await fix.createFile('x/y/z/spam.py'); + await sleep(2_000); // for filesystems with 1s resolution + const file3 = await fix.createFile('eggs.py'); + + const hash1 = await fileSystem.getFileHash(file1); + const hash2 = await fileSystem.getFileHash(file2); + const hash3 = await fileSystem.getFileHash(file3); + + expect(hash1).to.not.equal(hash2); + expect(hash1).to.not.equal(hash3); + expect(hash2).to.not.equal(hash3); + }); + + test('Getting hash for non existent file should throw error', async () => { + const promise = fileSystem.getFileHash(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('search', () => { + test('found matches', async () => { + const pattern = await fix.resolve(`x/y/z/spam.*`); + const expected: string[] = [ + await fix.createFile('x/y/z/spam.py'), + await fix.createFile('x/y/z/spam.pyc'), + await fix.createFile('x/y/z/spam.so'), + await fix.createDirectory('x/y/z/spam.data'), + ]; + // non-matches + await fix.createFile('x/spam.py'); + await fix.createFile('x/y/z/eggs.py'); + await fix.createFile('x/y/z/spam-all.py'); + await fix.createFile('x/y/z/spam'); + await fix.createFile('x/spam.py'); + await fix.createFile('x/y/z/.net.py'); + let files = await fileSystem.search(pattern); + + // For whatever reason, on Windows "search()" is + // returning filenames with forward slasshes... + files = files.map(fixPath); + expect(files.sort()).to.deep.equal(expected.sort()); + }); + test('found dot matches', async () => { + const dir = await fix.resolve(`x/y/z`); + const expected: string[] = [ + await fix.createFile('x/y/z/spam.py'), + await fix.createFile('x/y/z/.net.py'), + ]; + // non-matches + await fix.createFile('x/spam.py'); + await fix.createFile('x/y/z/spam'); + await fix.createFile('x/spam.py'); + let files = await fileSystem.search(`${dir}/**/*.py`, undefined, true); + + // For whatever reason, on Windows "search()" is + // returning filenames with forward slasshes... + files = files.map(fixPath); + expect(files.sort()).to.deep.equal(expected.sort()); + }); + + test('no matches', async () => { + const pattern = await fix.resolve(`x/y/z/spam.*`); + + const files = await fileSystem.search(pattern); + + expect(files).to.deep.equal([]); + }); + }); + + //============================= + // sync methods + + suite('fileExistsSync', () => { + test('want file, got file', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = fileSystem.fileExistsSync(filename); + + expect(exists).to.equal(true); + }); + + test('want file, not file', async () => { + const filename = await fix.createDirectory('x/y/z/spam.py'); + + const exists = fileSystem.fileExistsSync(filename); + + // Note that currently the "file" can be *anything*. It + // doesn't have to be just a regular file. This is the + // way it already worked, so we're keeping it that way + // for now. + expect(exists).to.equal(true); + }); + + test('symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + + const exists = fileSystem.fileExistsSync(symlink); + + // Note that currently the "file" can be *anything*. It + // doesn't have to be just a regular file. This is the + // way it already worked, so we're keeping it that way + // for now. + expect(exists).to.equal(true); + }); + + test('unknown', async function () { + if (!SUPPORTS_SOCKETS) { + this.skip(); + } + const sockFile = await fix.createSocket('x/y/z/ipc.sock'); + + const exists = fileSystem.fileExistsSync(sockFile); + + // Note that currently the "file" can be *anything*. It + // doesn't have to be just a regular file. This is the + // way it already worked, so we're keeping it that way + // for now. + expect(exists).to.equal(true); + }); + }); + }); +}); diff --git a/src/test/common/platform/filesystem.test.ts b/src/test/common/platform/filesystem.test.ts new file mode 100644 index 000000000000..a1afab02d1fe --- /dev/null +++ b/src/test/common/platform/filesystem.test.ts @@ -0,0 +1,1288 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +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'; +import { + assertDoesNotExist, + assertExists, + assertFileText, + DOES_NOT_EXIST, + FSFixture, + SUPPORTS_SOCKETS, + SUPPORTS_SYMLINKS, + WINDOWS, +} from './utils'; + +// Note: all functional tests that do not trigger the VS Code "fs" API +// are found in filesystem.functional.test.ts. + +suite('FileSystem - raw', () => { + let filesystem: IRawFileSystem; + let fix: FSFixture; + setup(async () => { + filesystem = RawFileSystem.withDefaults(); + fix = new FSFixture(); + + await assertDoesNotExist(DOES_NOT_EXIST); + }); + teardown(async () => { + await fix.cleanUp(); + }); + + suite('stat', () => { + setup(function () { + // https://github.com/microsoft/vscode-python/issues/10294 + + this.skip(); + }); + test('gets the info for an existing file', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const old = await fsextra.stat(filename); + const expected = convertStat(old, FileType.File); + + const stat = await filesystem.stat(filename); + + expect(stat).to.deep.equal(expected); + }); + + test('gets the info for an existing directory', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + const old = await fsextra.stat(dirname); + const expected = convertStat(old, FileType.Directory); + + const stat = await filesystem.stat(dirname); + + expect(stat).to.deep.equal(expected); + }); + + test('for symlinks, gets the info for the linked file', async function () { + if (!SUPPORTS_SYMLINKS) { + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + const old = await fsextra.stat(filename); + const expected = convertStat(old, FileType.SymbolicLink | FileType.File); + + const stat = await filesystem.stat(symlink); + + expect(stat).to.deep.equal(expected); + }); + + test('gets the info for a socket', async function () { + if (!SUPPORTS_SOCKETS) { + return this.skip(); + } + const sock = await fix.createSocket('x/spam.sock'); + const old = await fsextra.stat(sock); + const expected = convertStat(old, FileType.Unknown); + + const stat = await filesystem.stat(sock); + + expect(stat).to.deep.equal(expected); + }); + + test('fails if the file does not exist', async () => { + const promise = filesystem.stat(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('move', () => { + test('rename file', async () => { + const source = await fix.createFile('spam.py', '<text>'); + const target = await fix.resolve('eggs-txt'); + await assertDoesNotExist(target); + + await filesystem.move(source, target); + + await assertExists(target); + const text = await fsextra.readFile(target, 'utf8'); + expect(text).to.equal('<text>'); + await assertDoesNotExist(source); + }); + + test('rename directory', async () => { + const source = await fix.createDirectory('spam'); + await fix.createFile('spam/data.json', '<text>'); + const target = await fix.resolve('eggs'); + const filename = await fix.resolve('eggs/data.json', false); + await assertDoesNotExist(target); + + await filesystem.move(source, target); + + await assertExists(filename); + const text = await fsextra.readFile(filename, 'utf8'); + expect(text).to.equal('<text>'); + await assertDoesNotExist(source); + }); + + test('rename symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + this.skip(); + } + const filename = await fix.createFile('spam.py'); + const symlink = await fix.createSymlink('spam.lnk', filename); + const target = await fix.resolve('eggs'); + await assertDoesNotExist(target); + + await filesystem.move(symlink, target); + + await assertExists(target); + const linked = await fsextra.readlink(target); + expect(linked).to.equal(filename); + await assertDoesNotExist(symlink); + }); + + test('move file', async () => { + const source = await fix.createFile('spam.py', '<text>'); + await fix.createDirectory('eggs'); + const target = await fix.resolve('eggs/spam.py'); + await assertDoesNotExist(target); + + await filesystem.move(source, target); + + await assertExists(target); + const text = await fsextra.readFile(target, 'utf8'); + expect(text).to.equal('<text>'); + await assertDoesNotExist(source); + }); + + test('move directory', async () => { + const source = await fix.createDirectory('spam/spam/spam/eggs/spam'); + await fix.createFile('spam/spam/spam/eggs/spam/data.json', '<text>'); + await fix.createDirectory('spam/spam/spam/hash'); + const target = await fix.resolve('spam/spam/spam/hash/spam'); + const filename = await fix.resolve('spam/spam/spam/hash/spam/data.json', false); + await assertDoesNotExist(target); + + await filesystem.move(source, target); + + await assertExists(filename); + const text = await fsextra.readFile(filename, 'utf8'); + expect(text).to.equal('<text>'); + await assertDoesNotExist(source); + }); + + test('move symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + this.skip(); + } + const filename = await fix.createFile('spam.py'); + const symlink = await fix.createSymlink('w/spam.lnk', filename); + const target = await fix.resolve('x/spam.lnk'); + await assertDoesNotExist(target); + + await filesystem.move(symlink, target); + + await assertExists(target); + const linked = await fsextra.readlink(target); + expect(linked).to.equal(filename); + await assertDoesNotExist(symlink); + }); + + test('file target already exists', async () => { + const source = await fix.createFile('spam.py', '<text>'); + const target = await fix.createFile('eggs-txt', '<other>'); + + await filesystem.move(source, target); + + await assertDoesNotExist(source); + await assertExists(target); + const text2 = await fsextra.readFile(target, 'utf8'); + expect(text2).to.equal('<text>'); + }); + + test('directory target already exists', async () => { + const source = await fix.createDirectory('spam'); + const file3 = await fix.createFile('spam/data.json', '<text>'); + const target = await fix.createDirectory('eggs'); + const file1 = await fix.createFile('eggs/spam.py', '<code>'); + const file2 = await fix.createFile('eggs/data.json', '<other>'); + + const promise = filesystem.move(source, target); + + await expect(promise).to.eventually.be.rejected; + // Make sure nothing changed. + const text1 = await fsextra.readFile(file1, 'utf8'); + expect(text1).to.equal('<code>'); + const text2 = await fsextra.readFile(file2, 'utf8'); + expect(text2).to.equal('<other>'); + const text3 = await fsextra.readFile(file3, 'utf8'); + expect(text3).to.equal('<text>'); + }); + + test('fails if the file does not exist', async () => { + const source = await fix.resolve(DOES_NOT_EXIST); + const target = await fix.resolve('spam.py'); + + const promise = filesystem.move(source, target); + + await expect(promise).to.eventually.be.rejected; + // Make sure nothing changed. + await assertDoesNotExist(target); + }); + + test('fails if the target directory does not exist', async () => { + const source = await fix.createFile('x/spam.py', '<text>'); + const target = await fix.resolve('w/spam.py', false); + await assertDoesNotExist(path.dirname(target)); + + const promise = filesystem.move(source, target); + + await expect(promise).to.eventually.be.rejected; + // Make sure nothing changed. + await assertExists(source); + await assertDoesNotExist(target); + }); + }); + + suite('readData', () => { + test('returns contents of a file', async () => { + const text = '<some text>'; + const expected = Buffer.from(text, 'utf8'); + const filename = await fix.createFile('x/y/z/spam.py', text); + + const content = await filesystem.readData(filename); + + expect(content).to.deep.equal(expected); + }); + + test('throws an exception if file does not exist', async () => { + const promise = filesystem.readData(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('readText', () => { + test('returns contents of a file', async () => { + const expected = '<some text>'; + const filename = await fix.createFile('x/y/z/spam.py', expected); + + const content = await filesystem.readText(filename); + + expect(content).to.be.equal(expected); + }); + + test('always UTF-8', async () => { + const expected = '... 😁 ...'; + const filename = await fix.createFile('x/y/z/spam.py', expected); + + const text = await filesystem.readText(filename); + + expect(text).to.equal(expected); + }); + + test('returns garbage if encoding is UCS-2', async () => { + const filename = await fix.resolve('spam.py'); + // There are probably cases where this would fail too. + // However, the extension never has to deal with non-UTF8 + // cases, so it doesn't matter too much. + const original = '... 😁 ...'; + await fsextra.writeFile(filename, original, { encoding: 'ucs2' }); + + const text = await filesystem.readText(filename); + + expect(text).to.equal('.\u0000.\u0000.\u0000 \u0000=�\u0001� \u0000.\u0000.\u0000.\u0000'); + }); + + test('throws an exception if file does not exist', async () => { + const promise = filesystem.readText(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('writeText', () => { + test('creates the file if missing', async () => { + const filename = await fix.resolve('x/y/z/spam.py'); + await assertDoesNotExist(filename); + const data = 'line1\nline2\n'; + + await filesystem.writeText(filename, data); + + await assertFileText(filename, data); + }); + + test('always UTF-8', async () => { + const filename = await fix.resolve('x/y/z/spam.py'); + const data = '... 😁 ...'; + + await filesystem.writeText(filename, data); + + await assertFileText(filename, data); + }); + + test('overwrites existing file', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const data = 'line1\nline2\n'; + + await filesystem.writeText(filename, data); + + await assertFileText(filename, data); + }); + }); + + suite('copyFile', () => { + test('the source file gets copied (same directory)', async () => { + const data = '<content>'; + const src = await fix.createFile('x/y/z/spam.py', data); + const dest = await fix.resolve('x/y/z/spam.py.bak'); + await assertDoesNotExist(dest); + + await filesystem.copyFile(src, dest); + + await assertFileText(dest, data); + await assertFileText(src, data); // Make sure src wasn't changed. + }); + + test('the source file gets copied (different directory)', async () => { + const data = '<content>'; + const src = await fix.createFile('x/y/z/spam.py', data); + const dest = await fix.resolve('x/y/eggs.py'); + await assertDoesNotExist(dest); + + await filesystem.copyFile(src, dest); + + await assertFileText(dest, data); + await assertFileText(src, data); // Make sure src wasn't changed. + }); + + test('fails if the source does not exist', async () => { + const dest = await fix.resolve('x/spam.py'); + + const promise = filesystem.copyFile(DOES_NOT_EXIST, dest); + + await expect(promise).to.eventually.be.rejected; + }); + + test('fails if the target parent directory does not exist', async () => { + const src = await fix.createFile('x/spam.py', '...'); + const dest = await fix.resolve('y/eggs.py', false); + await assertDoesNotExist(path.dirname(dest)); + + const promise = filesystem.copyFile(src, dest); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('rmfile', () => { + test('deletes the file', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + await assertExists(filename); + + await filesystem.rmfile(filename); + + await assertDoesNotExist(filename); + }); + + test('fails if the file does not exist', async () => { + const promise = filesystem.rmfile(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('rmdir', () => { + test('deletes the directory if empty', async () => { + const dirname = await fix.createDirectory('x'); + await assertExists(dirname); + + await filesystem.rmdir(dirname); + + await assertDoesNotExist(dirname); + }); + + test('fails if the directory is not empty', async () => { + const dirname = await fix.createDirectory('x'); + const filename = await fix.createFile('x/y/z/spam.py'); + await assertExists(filename); + + const promise = filesystem.rmdir(dirname); + + await expect(promise).to.eventually.be.rejected; + }); + + test('fails if the directory does not exist', async () => { + const promise = filesystem.rmdir(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('rmtree', () => { + test('deletes the directory if empty', async () => { + const dirname = await fix.createDirectory('x'); + await assertExists(dirname); + + await filesystem.rmtree(dirname); + + await assertDoesNotExist(dirname); + }); + + test('deletes the directory if not empty', async () => { + const dirname = await fix.createDirectory('x'); + const filename = await fix.createFile('x/y/z/spam.py'); + await assertExists(filename); + + await filesystem.rmtree(dirname); + + await assertDoesNotExist(dirname); + }); + + test('fails if the directory does not exist', async () => { + const promise = filesystem.rmtree(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('mkdirp', () => { + test('creates the directory and all missing parents', async () => { + await fix.createDirectory('x'); + // x/y, x/y/z, and x/y/z/spam are all missing. + const dirname = await fix.resolve('x/y/z/spam', false); + await assertDoesNotExist(dirname); + + await filesystem.mkdirp(dirname); + + await assertExists(dirname); + }); + + test('works if the directory already exists', async () => { + const dirname = await fix.createDirectory('spam'); + await assertExists(dirname); + + await filesystem.mkdirp(dirname); + + await assertExists(dirname); + }); + }); + + suite('listdir', () => { + test('mixed', async function () { + // https://github.com/microsoft/vscode-python/issues/10240 + + return this.skip(); + // Create the target directory and its contents. + const dirname = await fix.createDirectory('x/y/z'); + const file1 = await fix.createFile('x/y/z/__init__.py', ''); + const script = await fix.createFile('x/y/z/__main__.py', '<script here>'); + const file2 = await fix.createFile('x/y/z/spam.py', '...'); + const file3 = await fix.createFile('x/y/z/eggs.py', '"""..."""'); + const subdir = await fix.createDirectory('x/y/z/w'); + const expected = [ + [file1, FileType.File], + [script, FileType.File], + [file3, FileType.File], + [file2, FileType.File], + [subdir, FileType.Directory], + ]; + if (SUPPORTS_SYMLINKS) { + // a symlink to a file (source not directly in listed dir) + const symlink1 = await fix.createSymlink( + 'x/y/z/info.py', + // Link to an ignored file. + await fix.createFile('x/_info.py', '<info here>'), // source + ); + expected.push([symlink1, FileType.SymbolicLink | FileType.File]); + + // a symlink to a directory (source not directly in listed dir) + const symlink4 = await fix.createSymlink( + 'x/y/z/static_files', + await fix.resolve('x/y/z/w/data'), // source + ); + expected.push([symlink4, FileType.SymbolicLink | FileType.Directory]); + + // a broken symlink + // TODO (https://github.com/microsoft/vscode/issues/90031): + // VS Code ignores broken symlinks currently... + //const symlink2 = await fix.createSymlink( + // 'x/y/z/broken', + // DOES_NOT_EXIST // source + //); + //expected.push([symlink2, FileType.SymbolicLink]); + } + if (SUPPORTS_SOCKETS) { + // a socket + const sock = await fix.createSocket('x/y/z/ipc.sock'); + expected.push([sock, FileType.Unknown]); + + if (SUPPORTS_SYMLINKS) { + // a symlink to a socket + const symlink3 = await fix.createSymlink( + 'x/y/z/ipc.sck', + sock, // source + ); + expected.push( + // TODO (https://github.com/microsoft/vscode/issues/90032): + // VS Code gets symlinks to "unknown" files wrong: + [symlink3, FileType.SymbolicLink | FileType.File], + //[symlink3, FileType.SymbolicLink] + ); + } + } + // Create other files and directories (should be ignored). + await fix.createFile('x/__init__.py', ''); + await fix.createFile('x/y/__init__.py', ''); + await fix.createDirectory('x/y/z/w/data'); + await fix.createFile('x/y/z/w/data/v1.json'); + if (SUPPORTS_SYMLINKS) { + // a broken symlink + // TODO (https://github.com/microsoft/vscode/issues/90031): + // VS Code ignores broken symlinks currently... + await fix.createSymlink( + 'x/y/z/broken', + DOES_NOT_EXIST, // source + ); + + // a symlink outside the listed dir (to a file inside the dir) + await fix.createSymlink( + 'my-script.py', + // Link to a listed file. + script, // source (__main__.py) + ); + + // a symlink in a subdir (to a file outside the dir) + await fix.createSymlink( + 'x/y/z/w/__init__.py', + await fix.createFile('x/__init__.py', ''), // source + ); + } + + const entries = await filesystem.listdir(dirname); + + expect(entries.sort()).to.deep.equal(expected.sort()); + }); + + test('empty', async () => { + const dirname = await fix.createDirectory('x/y/z/eggs'); + + const entries = await filesystem.listdir(dirname); + + expect(entries).to.deep.equal([]); + }); + + test('fails if the directory does not exist', async () => { + const promise = filesystem.listdir(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); +}); + +suite('FileSystem - utils', () => { + let utils: IFileSystemUtils; + let fix: FSFixture; + setup(async () => { + utils = FileSystemUtils.withDefaults(); + fix = new FSFixture(); + + await assertDoesNotExist(DOES_NOT_EXIST); + }); + teardown(async () => { + await fix.cleanUp(); + }); + + suite('createDirectory', () => { + test('wraps the low-level impl', async () => { + await fix.createDirectory('x'); + // x/y, x/y/z, and x/y/z/spam are all missing. + const dirname = await fix.resolve('x/spam', false); + await assertDoesNotExist(dirname); + + await utils.createDirectory(dirname); + + await assertExists(dirname); + }); + }); + + suite('deleteDirectory', () => { + test('wraps the low-level impl', async () => { + const dirname = await fix.createDirectory('x'); + await assertExists(dirname); + + await utils.deleteDirectory(dirname); + + await assertDoesNotExist(dirname); + }); + }); + + suite('deleteFile', () => { + test('wraps the low-level impl', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + await assertExists(filename); + + await utils.deleteFile(filename); + + await assertDoesNotExist(filename); + }); + }); + + suite('pathExists', () => { + test('exists (without type)', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = await utils.pathExists(filename); + + expect(exists).to.equal(true); + }); + + test('does not exist (without type)', async () => { + const exists = await utils.pathExists(DOES_NOT_EXIST); + + expect(exists).to.equal(false); + }); + + test('matches (type: file)', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = await utils.pathExists(filename, FileType.File); + + expect(exists).to.equal(true); + }); + + test('mismatch (type: file)', async () => { + const filename = await fix.createDirectory('x/y/z/spam.py'); + + const exists = await utils.pathExists(filename, FileType.File); + + expect(exists).to.equal(false); + }); + + test('matches (type: directory)', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + + const exists = await utils.pathExists(dirname, FileType.Directory); + + expect(exists).to.equal(true); + }); + + test('mismatch (type: directory)', async () => { + const dirname = await fix.createFile('x/y/z/spam'); + + const exists = await utils.pathExists(dirname, FileType.Directory); + + expect(exists).to.equal(false); + }); + + test('symlinks are followed', async function () { + if (!SUPPORTS_SYMLINKS) { + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + + const exists = await utils.pathExists(symlink, FileType.SymbolicLink); + const destIsFile = await utils.pathExists(symlink, FileType.File); + const destIsDir = await utils.pathExists(symlink, FileType.Directory); + + expect(exists).to.equal(true); + expect(destIsFile).to.equal(true); + expect(destIsDir).to.equal(false); + }); + + test('mismatch (type: symlink)', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = await utils.pathExists(filename, FileType.SymbolicLink); + + expect(exists).to.equal(false); + }); + + test('matches (type: unknown)', async function () { + if (!SUPPORTS_SOCKETS) { + this.skip(); + } + const sockFile = await fix.createSocket('x/y/z/ipc.sock'); + + const exists = await utils.pathExists(sockFile, FileType.Unknown); + + expect(exists).to.equal(true); + }); + + test('mismatch (type: unknown)', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = await utils.pathExists(filename, FileType.Unknown); + + expect(exists).to.equal(false); + }); + }); + + suite('fileExists', () => { + test('want file, got file', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = await utils.fileExists(filename); + + expect(exists).to.equal(true); + }); + + test('want file, not file', async () => { + const filename = await fix.createDirectory('x/y/z/spam.py'); + + const exists = await utils.fileExists(filename); + + expect(exists).to.equal(false); + }); + + test('symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + + const exists = await utils.fileExists(symlink); + + // This is because we currently use stat() and not lstat(). + expect(exists).to.equal(true); + }); + + test('unknown', async function () { + if (!SUPPORTS_SOCKETS) { + this.skip(); + } + const sockFile = await fix.createSocket('x/y/z/ipc.sock'); + + const exists = await utils.fileExists(sockFile); + + expect(exists).to.equal(false); + }); + + test('failure in stat()', async function () { + if (WINDOWS) { + this.skip(); + } + const dirname = await fix.createDirectory('x/y/z'); + const filename = await fix.createFile('x/y/z/spam.py', '...'); + await fsextra.chmod(dirname, 0o400); + + let exists: boolean; + try { + exists = await utils.fileExists(filename); + } finally { + await fsextra.chmod(dirname, 0o755); + } + + expect(exists).to.equal(false); + }); + }); + + suite('directoryExists', () => { + test('want directory, got directory', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + + const exists = await utils.directoryExists(dirname); + + expect(exists).to.equal(true); + }); + + test('want directory, not directory', async () => { + const dirname = await fix.createFile('x/y/z/spam'); + + const exists = await utils.directoryExists(dirname); + + expect(exists).to.equal(false); + }); + + test('symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + this.skip(); + } + const dirname = await fix.createDirectory('x/y/z/spam'); + const symlink = await fix.createSymlink('x/y/z/eggs', dirname); + + const exists = await utils.directoryExists(symlink); + + // This is because we currently use stat() and not lstat(). + expect(exists).to.equal(true); + }); + + test('unknown', async function () { + if (!SUPPORTS_SOCKETS) { + this.skip(); + } + const sockFile = await fix.createSocket('x/y/z/ipc.sock'); + + const exists = await utils.directoryExists(sockFile); + + expect(exists).to.equal(false); + }); + + test('failure in stat()', async function () { + if (WINDOWS) { + this.skip(); + } + const parentdir = await fix.createDirectory('x/y/z'); + const dirname = await fix.createDirectory('x/y/z/spam'); + await fsextra.chmod(parentdir, 0o400); + + let exists: boolean; + try { + exists = await utils.fileExists(dirname); + } finally { + await fsextra.chmod(parentdir, 0o755); + } + + expect(exists).to.equal(false); + }); + }); + + suite('listdir', () => { + test('wraps the low-level impl', async () => { + test('mixed', async () => { + // Create the target directory and its contents. + const dirname = await fix.createDirectory('x/y/z'); + const file = await fix.createFile('x/y/z/__init__.py', ''); + const subdir = await fix.createDirectory('x/y/z/w'); + + const entries = await utils.listdir(dirname); + + expect(entries.sort()).to.deep.equal([ + [file, FileType.File], + [subdir, FileType.Directory], + ]); + }); + }); + }); + + suite('getSubDirectories', () => { + test('empty if the directory does not exist', async () => { + const entries = await utils.getSubDirectories(DOES_NOT_EXIST); + + expect(entries).to.deep.equal([]); + }); + }); + + suite('getFiles', () => { + test('empty if the directory does not exist', async () => { + const entries = await utils.getFiles(DOES_NOT_EXIST); + + expect(entries).to.deep.equal([]); + }); + }); + + suite('isDirReadonly', () => { + suite('non-Windows', () => { + suiteSetup(function () { + if (WINDOWS) { + this.skip(); + } + }); + + // On Windows, chmod won't have any effect on the file itself. + test('is readonly', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + await fsextra.chmod(dirname, 0o444); + + const isReadonly = await utils.isDirReadonly(dirname); + + expect(isReadonly).to.equal(true); + }); + }); + + test('is not readonly', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + + const isReadonly = await utils.isDirReadonly(dirname); + + expect(isReadonly).to.equal(false); + }); + + test('fail if the directory does not exist', async () => { + const promise = utils.isDirReadonly(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); +}); + +suite('FileSystem', () => { + let filesystem: IFileSystem; + let fix: FSFixture; + setup(async () => { + filesystem = new FileSystem(); + fix = new FSFixture(); + + await assertDoesNotExist(DOES_NOT_EXIST); + }); + teardown(async () => { + await fix.cleanUp(); + }); + + suite('raw', () => { + suite('stat', () => { + setup(function () { + // https://github.com/microsoft/vscode-python/issues/10294 + + this.skip(); + }); + test('gets the info for an existing file', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const old = await fsextra.stat(filename); + const expected = convertStat(old, FileType.File); + + const stat = await filesystem.stat(filename); + + expect(stat).to.deep.equal(expected); + }); + + test('gets the info for an existing directory', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + const old = await fsextra.stat(dirname); + const expected = convertStat(old, FileType.Directory); + + const stat = await filesystem.stat(dirname); + + expect(stat).to.deep.equal(expected); + }); + + test('for symlinks, gets the info for the linked file', async function () { + // https://github.com/microsoft/vscode-python/issues/10294 + + this.skip(); + if (!SUPPORTS_SYMLINKS) { + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + const old = await fsextra.stat(filename); + const expected = convertStat(old, FileType.SymbolicLink | FileType.File); + + const stat = await filesystem.stat(symlink); + + expect(stat).to.deep.equal(expected); + }); + + test('gets the info for a socket', async function () { + if (!SUPPORTS_SOCKETS) { + return this.skip(); + } + const sock = await fix.createSocket('x/spam.sock'); + const old = await fsextra.stat(sock); + const expected = convertStat(old, FileType.Unknown); + + const stat = await filesystem.stat(sock); + + expect(stat).to.deep.equal(expected); + }); + + test('fails if the file does not exist', async () => { + const promise = filesystem.stat(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + + suite('createDirectory', () => { + test('wraps the low-level impl', async () => { + await fix.createDirectory('x'); + // x/y, x/y/z, and x/y/z/spam are all missing. + const dirname = await fix.resolve('x/spam', false); + await assertDoesNotExist(dirname); + + await filesystem.createDirectory(dirname); + + await assertExists(dirname); + }); + }); + + suite('deleteDirectory', () => { + test('wraps the low-level impl', async () => { + const dirname = await fix.createDirectory('x'); + await assertExists(dirname); + + await filesystem.deleteDirectory(dirname); + + await assertDoesNotExist(dirname); + }); + }); + + suite('listdir', () => { + test('wraps the low-level impl', async () => { + test('mixed', async () => { + // Create the target directory and its contents. + const dirname = await fix.createDirectory('x/y/z'); + const file = await fix.createFile('x/y/z/__init__.py', ''); + const subdir = await fix.createDirectory('x/y/z/w'); + + const entries = await filesystem.listdir(dirname); + + expect(entries.sort()).to.deep.equal([ + [file, FileType.File], + [subdir, FileType.Directory], + ]); + }); + }); + }); + + suite('readFile', () => { + test('wraps the low-level impl', async () => { + const expected = '<some text>'; + const filename = await fix.createFile('x/y/z/spam.py', expected); + + const content = await filesystem.readFile(filename); + + expect(content).to.be.equal(expected); + }); + }); + + suite('readData', () => { + test('wraps the low-level impl', async () => { + const text = '<some text>'; + const expected = Buffer.from(text, 'utf8'); + const filename = await fix.createFile('x/y/z/spam.py', text); + + const content = await filesystem.readData(filename); + + expect(content).to.deep.equal(expected); + }); + }); + + suite('writeFile', () => { + test('wraps the low-level impl', async () => { + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const data = 'line1\nline2\n'; + + await filesystem.writeFile(filename, data); + + await assertFileText(filename, data); + }); + }); + + suite('copyFile', () => { + test('wraps the low-level impl', async () => { + const data = '<content>'; + const src = await fix.createFile('x/y/z/spam.py', data); + const dest = await fix.resolve('x/y/z/spam.py.bak'); + await assertDoesNotExist(dest); + + await filesystem.copyFile(src, dest); + + await assertFileText(dest, data); + await assertFileText(src, data); // Make sure src wasn't changed. + }); + }); + + suite('move', () => { + test('wraps the low-level impl', async () => { + const source = await fix.createFile('spam.py', '<text>'); + const target = await fix.resolve('eggs-txt'); + await assertDoesNotExist(target); + + await filesystem.move(source, target); + + await assertExists(target); + const text = await fsextra.readFile(target, 'utf8'); + expect(text).to.equal('<text>'); + await assertDoesNotExist(source); + }); + }); + }); + + suite('utils', () => { + suite('fileExists', () => { + test('want file, got file', async () => { + const filename = await fix.createFile('x/y/z/spam.py'); + + const exists = await filesystem.fileExists(filename); + + expect(exists).to.equal(true); + }); + + test('want file, not file', async () => { + const filename = await fix.createDirectory('x/y/z/spam.py'); + + const exists = await filesystem.fileExists(filename); + + expect(exists).to.equal(false); + }); + + test('symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + this.skip(); + } + const filename = await fix.createFile('x/y/z/spam.py', '...'); + const symlink = await fix.createSymlink('x/y/z/eggs.py', filename); + + const exists = await filesystem.fileExists(symlink); + + // This is because we currently use stat() and not lstat(). + expect(exists).to.equal(true); + }); + + test('unknown', async function () { + if (!SUPPORTS_SOCKETS) { + this.skip(); + } + const sockFile = await fix.createSocket('x/y/z/ipc.sock'); + + const exists = await filesystem.fileExists(sockFile); + + expect(exists).to.equal(false); + }); + }); + + suite('directoryExists', () => { + test('want directory, got directory', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + + const exists = await filesystem.directoryExists(dirname); + + expect(exists).to.equal(true); + }); + + test('want directory, not directory', async () => { + const dirname = await fix.createFile('x/y/z/spam'); + + const exists = await filesystem.directoryExists(dirname); + + expect(exists).to.equal(false); + }); + + test('symlink', async function () { + if (!SUPPORTS_SYMLINKS) { + this.skip(); + } + const dirname = await fix.createDirectory('x/y/z/spam'); + const symlink = await fix.createSymlink('x/y/z/eggs', dirname); + + const exists = await filesystem.directoryExists(symlink); + + // This is because we currently use stat() and not lstat(). + expect(exists).to.equal(true); + }); + + test('unknown', async function () { + if (!SUPPORTS_SOCKETS) { + this.skip(); + } + const sockFile = await fix.createSocket('x/y/z/ipc.sock'); + + const exists = await filesystem.directoryExists(sockFile); + + expect(exists).to.equal(false); + }); + }); + + suite('getSubDirectories', () => { + test('mixed types', async () => { + // Create the target directory and its subdirs. + const dirname = await fix.createDirectory('x/y/z/scripts'); + const expected = [ + await fix.createDirectory('x/y/z/scripts/w'), // subdir1 + await fix.createDirectory('x/y/z/scripts/v'), // subdir2 + ]; + if (SUPPORTS_SYMLINKS) { + // a symlink to a directory (source is outside listed dir) + const symlinkDirSource = await fix.createDirectory('x/data'); + const symlink = await fix.createSymlink('x/y/z/scripts/datadir', symlinkDirSource); + expected.push(symlink); + } + // Create files in the directory (should be ignored). + await fix.createFile('x/y/z/scripts/spam.py'); + await fix.createFile('x/y/z/scripts/eggs.py'); + await fix.createFile('x/y/z/scripts/data.json'); + if (SUPPORTS_SYMLINKS) { + // a symlink to a file (source outside listed dir) + const symlinkFileSource = await fix.createFile('x/info.py'); + await fix.createSymlink('x/y/z/scripts/other', symlinkFileSource); + } + if (SUPPORTS_SOCKETS) { + // a plain socket + await fix.createSocket('x/y/z/scripts/spam.sock'); + } + + const results = await filesystem.getSubDirectories(dirname); + + expect(results.sort()).to.deep.equal(expected.sort()); + }); + + test('empty if the directory does not exist', async () => { + const entries = await filesystem.getSubDirectories(DOES_NOT_EXIST); + + expect(entries).to.deep.equal([]); + }); + }); + + suite('getFiles', () => { + test('mixed types', async () => { + // Create the target directory and its files. + const dirname = await fix.createDirectory('x/y/z/scripts'); + const expected = [ + await fix.createFile('x/y/z/scripts/spam.py'), // file1 + await fix.createFile('x/y/z/scripts/eggs.py'), // file2 + await fix.createFile('x/y/z/scripts/data.json'), // file3 + ]; + if (SUPPORTS_SYMLINKS) { + const symlinkFileSource = await fix.createFile('x/info.py'); + const symlink = await fix.createSymlink('x/y/z/scripts/other', symlinkFileSource); + expected.push(symlink); + } + // Create subdirs, sockets, etc. in the directory (should be ignored). + await fix.createDirectory('x/y/z/scripts/w'); + await fix.createDirectory('x/y/z/scripts/v'); + if (SUPPORTS_SYMLINKS) { + const symlinkDirSource = await fix.createDirectory('x/data'); + await fix.createSymlink('x/y/z/scripts/datadir', symlinkDirSource); + } + if (SUPPORTS_SOCKETS) { + await fix.createSocket('x/y/z/scripts/spam.sock'); + } + + const results = await filesystem.getFiles(dirname); + + expect(results.sort()).to.deep.equal(expected.sort()); + }); + + test('empty if the directory does not exist', async () => { + const entries = await filesystem.getFiles(DOES_NOT_EXIST); + + expect(entries).to.deep.equal([]); + }); + }); + + suite('isDirReadonly', () => { + suite('non-Windows', () => { + suiteSetup(function () { + if (WINDOWS) { + this.skip(); + } + }); + + // On Windows, chmod won't have any effect on the file itself. + test('is readonly', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + await fsextra.chmod(dirname, 0o444); + + const isReadonly = await filesystem.isDirReadonly(dirname); + + expect(isReadonly).to.equal(true); + }); + }); + + test('is not readonly', async () => { + const dirname = await fix.createDirectory('x/y/z/spam'); + + const isReadonly = await filesystem.isDirReadonly(dirname); + + expect(isReadonly).to.equal(false); + }); + + test('fail if the directory does not exist', async () => { + const promise = filesystem.isDirReadonly(DOES_NOT_EXIST); + + await expect(promise).to.eventually.be.rejected; + }); + }); + }); +}); diff --git a/src/test/common/platform/filesystem.unit.test.ts b/src/test/common/platform/filesystem.unit.test.ts index d908e8165f84..f012cb9fb27e 100644 --- a/src/test/common/platform/filesystem.unit.test.ts +++ b/src/test/common/platform/filesystem.unit.test.ts @@ -1,118 +1,1476 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { expect, use } from 'chai'; -import * as fs from 'fs-extra'; -import * as path from 'path'; +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as fsextra from '../../../client/common/platform/fs-paths'; import * as TypeMoq from 'typemoq'; -import { FileSystem } from '../../../client/common/platform/fileSystem'; -import { IFileSystem, IPlatformService, TemporaryFile } from '../../../client/common/platform/types'; -// tslint:disable-next-line:no-require-imports no-var-requires -const assertArrays = require('chai-arrays'); -use(assertArrays); - -// tslint:disable-next-line:max-func-body-length -suite('FileSystem', () => { - let platformService: TypeMoq.IMock<IPlatformService>; - let fileSystem: IFileSystem; - const fileToAppendTo = path.join(__dirname, 'created_for_testing_dummy.txt'); +import * as vscode from 'vscode'; +import { FileSystemUtils, RawFileSystem } from '../../../client/common/platform/fileSystem'; +import { + FileStat, + FileType, + // These interfaces are needed for FileSystemUtils deps. + IFileSystemPaths, + IFileSystemPathUtils, + IRawFileSystem, + ITempFileSystem, + ReadStream, + WriteStream, +} from '../../../client/common/platform/types'; + +function Uri(filename: string): vscode.Uri { + return vscode.Uri.file(filename); +} + +function createDummyStat(filetype: FileType): FileStat { + return { type: filetype } as any; +} + +function copyStat(stat: FileStat, old: TypeMoq.IMock<fsextra.Stats>) { + old.setup((s) => s.size) // plug in the original value + .returns(() => stat.size); + old.setup((s) => s.ctimeMs) // plug in the original value + .returns(() => stat.ctime); + old.setup((s) => s.mtimeMs) // plug in the original value + .returns(() => stat.mtime); +} + +interface IPaths { + // fs paths (IFileSystemPaths) + sep: string; + dirname(filename: string): string; + join(...paths: string[]): string; +} + +interface IRawFS extends IPaths { + // vscode.workspace.fs + copy(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable<void>; + createDirectory(uri: vscode.Uri): Thenable<void>; + delete(uri: vscode.Uri, options?: { recursive: boolean; useTrash: boolean }): Thenable<void>; + readDirectory(uri: vscode.Uri): Thenable<[string, FileType][]>; + readFile(uri: vscode.Uri): Thenable<Uint8Array>; + rename(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable<void>; + stat(uri: vscode.Uri): Thenable<FileStat>; + writeFile(uri: vscode.Uri, content: Uint8Array): Thenable<void>; + + // "fs-extra" + 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>; + lstatSync(filename: string): fs.Stats; + statSync(filename: string): fs.Stats; + readFileSync(path: string, encoding: string): string; + createReadStream(filename: string): ReadStream; + createWriteStream(filename: string): WriteStream; +} + +suite('Raw FileSystem', () => { + let raw: TypeMoq.IMock<IRawFS>; + let oldStats: TypeMoq.IMock<fs.Stats>[]; + let filesystem: RawFileSystem; setup(() => { - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - fileSystem = new FileSystem(platformService.object); - cleanTestFiles(); - }); - teardown(cleanTestFiles); - function cleanTestFiles() { - if (fs.existsSync(fileToAppendTo)) { - fs.unlinkSync(fileToAppendTo); + raw = TypeMoq.Mock.ofType<IRawFS>(undefined, TypeMoq.MockBehavior.Strict); + oldStats = []; + filesystem = new RawFileSystem( + // Since it's a mock we can just use it for all 3 values. + raw.object, + raw.object, + raw.object, + ); + }); + function verifyAll() { + raw.verifyAll(); + oldStats.forEach((stat) => { + stat.verifyAll(); + }); + } + function createMockLegacyStat(): TypeMoq.IMock<fsextra.Stats> { + const stat = TypeMoq.Mock.ofType<fsextra.Stats>(undefined, TypeMoq.MockBehavior.Strict); + // This is necessary because passing "mock.object" to + // Promise.resolve() triggers the lookup. + stat.setup((s: any) => s.then) + .returns(() => undefined) + .verifiable(TypeMoq.Times.atLeast(0)); + oldStats.push(stat); + return stat; + } + function setupStatFileType(stat: TypeMoq.IMock<fs.Stats>, filetype: FileType) { + // This mirrors the logic in convertFileType(). + if (filetype === FileType.File) { + stat.setup((s) => s.isFile()) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + } else if (filetype === FileType.Directory) { + stat.setup((s) => s.isFile()) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + stat.setup((s) => s.isDirectory()) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + } else if ((filetype & FileType.SymbolicLink) > 0) { + stat.setup((s) => s.isFile()) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + stat.setup((s) => s.isDirectory()) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + stat.setup((s) => s.isSymbolicLink()) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + } else if (filetype === FileType.Unknown) { + stat.setup((s) => s.isFile()) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + stat.setup((s) => s.isDirectory()) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + stat.setup((s) => s.isSymbolicLink()) + .returns(() => false) + .verifiable(TypeMoq.Times.atLeastOnce()); + } else { + throw Error(`unsupported file type ${filetype}`); } } - test('ReadFile returns contents of a file', async () => { - const file = __filename; - const expectedContents = await fs.readFile(file).then(buffer => buffer.toString()); - const content = await fileSystem.readFile(file); - expect(content).to.be.equal(expectedContents); + suite('stat', () => { + test('wraps the low-level function', async () => { + const filename = 'x/y/z/spam.py'; + const expected = createDummyStat(FileType.File); + raw.setup((r) => r.stat(Uri(filename))) // expect the specific filename + .returns(() => Promise.resolve(expected)); + + const stat = await filesystem.stat(filename); + + expect(stat).to.equal(expected); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.stat(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.stat('spam.py'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); }); - test('ReadFile throws an exception if file does not exist', async () => { - const readPromise = fs.readFile('xyz', { encoding: 'utf8' }); - await expect(readPromise).to.be.rejectedWith(); + suite('lstat', () => { + [ + { kind: 'file', filetype: FileType.File }, + { kind: 'dir', filetype: FileType.Directory }, + { kind: 'symlink', filetype: FileType.SymbolicLink }, + { kind: 'unknown', filetype: FileType.Unknown }, + ].forEach((testData) => { + test(`wraps the low-level function (filetype: ${testData.kind}`, async () => { + const filename = 'x/y/z/spam.py'; + const expected: FileStat = { + type: testData.filetype, + size: 10, + ctime: 101, + mtime: 102, + } as any; + const old = createMockLegacyStat(); + setupStatFileType(old, testData.filetype); + copyStat(expected, old); + raw.setup((r) => r.lstat(filename)) // expect the specific filename + .returns(() => Promise.resolve(old.object)); + + const stat = await filesystem.lstat(filename); + + expect(stat).to.deep.equal(expected); + verifyAll(); + }); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.lstat(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.lstat('spam.py'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); }); - function caseSensitivityFileCheck(isWindows: boolean, isOsx: boolean, isLinux: boolean) { - platformService.setup(p => p.isWindows).returns(() => isWindows); - platformService.setup(p => p.isMac).returns(() => isOsx); - platformService.setup(p => p.isLinux).returns(() => isLinux); - const path1 = 'c:\\users\\Peter Smith\\my documents\\test.txt'; - const path2 = 'c:\\USERS\\Peter Smith\\my documents\\test.TXT'; - const path3 = 'c:\\USERS\\Peter Smith\\my documents\\test.exe'; + suite('chmod', () => { + test('passes through a string mode', async () => { + const filename = 'x/y/z/spam.py'; + const mode = '755'; + raw.setup((r) => r.chmod(filename, mode)) // expect the specific filename + .returns(() => Promise.resolve()); - if (isWindows) { - expect(fileSystem.arePathsSame(path1, path2)).to.be.equal(true, 'file paths do not match (windows)'); - } else { - expect(fileSystem.arePathsSame(path1, path2)).to.be.equal(false, 'file match (non windows)'); - } + await filesystem.chmod(filename, mode); - expect(fileSystem.arePathsSame(path1, path1)).to.be.equal(true, '1. file paths do not match'); - expect(fileSystem.arePathsSame(path2, path2)).to.be.equal(true, '2. file paths do not match'); - expect(fileSystem.arePathsSame(path1, path3)).to.be.equal(false, '2. file paths do not match'); - } + verifyAll(); + }); + + test('passes through an int mode', async () => { + const filename = 'x/y/z/spam.py'; + const mode = 0o755; + raw.setup((r) => r.chmod(filename, mode)) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.chmod(filename, mode); + + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.chmod(TypeMoq.It.isAny(), TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.chmod('spam.py', 755); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('move', () => { + test('move a file (target does not exist)', async () => { + const src = 'x/y/z/spam.py'; + const tgt = 'x/y/spam.py'; + raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. + .returns(() => 'x/y'); + raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.move(src, tgt); + + verifyAll(); + }); + + test('move a file (target exists)', async () => { + const src = 'x/y/z/spam.py'; + const tgt = 'x/y/spam.py'; + raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. + .returns(() => 'x/y'); + raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + const err = vscode.FileSystemError.FileExists('...'); + raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename + .returns(() => Promise.reject(err)); + raw.setup((r) => r.stat(Uri(tgt))) // It's a file. + .returns(() => Promise.resolve(({ type: FileType.File } as unknown) as FileStat)); + raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: true })) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.move(src, tgt); + + verifyAll(); + }); + + test('move a directory (target does not exist)', async () => { + const src = 'x/y/z/spam'; + const tgt = 'x/y/spam'; + raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. + .returns(() => 'x/y'); + raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.move(src, tgt); + + verifyAll(); + }); + + test('moving a directory fails if target exists', async () => { + const src = 'x/y/z/spam.py'; + const tgt = 'x/y/spam.py'; + raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. + .returns(() => 'x/y'); + raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + const err = vscode.FileSystemError.FileExists('...'); + raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename + .returns(() => Promise.reject(err)); + raw.setup((r) => r.stat(Uri(tgt))) // It's a directory. + .returns(() => Promise.resolve(({ type: FileType.Directory } as unknown) as FileStat)); + + const promise = filesystem.move(src, tgt); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + + test('move a symlink to a directory (target exists)', async () => { + const src = 'x/y/z/spam'; + const tgt = 'x/y/spam.lnk'; + raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. + .returns(() => 'x/y'); + raw.setup((r) => r.stat(Uri('x/y'))) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + const err = vscode.FileSystemError.FileExists('...'); + raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: false })) // expect the specific filename + .returns(() => Promise.reject(err)); + raw.setup((r) => r.stat(Uri(tgt))) // It's a symlink. + .returns(() => + Promise.resolve(({ type: FileType.SymbolicLink | FileType.Directory } as unknown) as FileStat), + ); + raw.setup((r) => r.rename(Uri(src), Uri(tgt), { overwrite: true })) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.move(src, tgt); + + verifyAll(); + }); + + test('fails if the target parent dir does not exist', async () => { + raw.setup((r) => r.dirname(TypeMoq.It.isAny())) // Provide the target's parent. + .returns(() => ''); + const err = vscode.FileSystemError.FileNotFound('...'); + raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The parent dir does not exist. + .returns(() => Promise.reject(err)); + + const promise = filesystem.move('spam', 'eggs'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.dirname(TypeMoq.It.isAny())) // Provide the target's parent. + .returns(() => ''); + raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + const err = new Error('oops!'); + raw.setup((r) => r.rename(TypeMoq.It.isAny(), TypeMoq.It.isAny(), { overwrite: false })) // We don't care about the filename. + .throws(err); + + const promise = filesystem.move('spam', 'eggs'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('readData', () => { + test('wraps the low-level function', async () => { + const filename = 'x/y/z/spam.py'; + const expected = Buffer.from('<data>'); + raw.setup((r) => r.readFile(Uri(filename))) // expect the specific filename + .returns(() => Promise.resolve(expected)); + + const data = await filesystem.readData(filename); + + expect(data).to.deep.equal(expected); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.readFile(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.readData('spam.py'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('readText', () => { + test('wraps the low-level function', async () => { + const filename = 'x/y/z/spam.py'; + const expected = '<text>'; + const data = Buffer.from(expected); + raw.setup((r) => r.readFile(Uri(filename))) // expect the specific filename + .returns(() => Promise.resolve(data)); + + const text = await filesystem.readText(filename); + + expect(text).to.equal(expected); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.readFile(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.readText('spam.py'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('writeText', () => { + test('wraps the low-level function', async () => { + const filename = 'x/y/z/spam.py'; + const text = '<text>'; + const data = Buffer.from(text); + raw.setup((r) => r.writeFile(Uri(filename), data)) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.writeText(filename, text); + + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.writeFile(TypeMoq.It.isAny(), TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.writeText('spam.py', '<text>'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('appendText', () => { + test('wraps the low-level function', async () => { + const filename = 'x/y/z/spam.py'; + const text = '<text>'; + raw.setup((r) => r.appendFile(filename, text)) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.appendText(filename, text); - test('Case sensitivity is ignored when comparing file names on windows', async () => { - caseSensitivityFileCheck(true, false, false); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.appendFile(TypeMoq.It.isAny(), TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.appendText('spam.py', '<text>'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); }); - test('Case sensitivity is not ignored when comparing file names on osx', async () => { - caseSensitivityFileCheck(false, true, false); + suite('copyFile', () => { + test('wraps the low-level function', async () => { + const src = 'x/y/z/spam.py'; + const tgt = 'x/y/z/eggs.py'; + raw.setup((r) => r.dirname(tgt)) // Provide the target's parent. + .returns(() => 'x/y/z'); + raw.setup((r) => r.stat(Uri('x/y/z'))) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + raw.setup((r) => r.copy(Uri(src), Uri(tgt), { overwrite: true })) // Expect the specific args. + .returns(() => Promise.resolve()); + + await filesystem.copyFile(src, tgt); + + verifyAll(); + }); + + test('fails if target parent does not exist', async () => { + raw.setup((r) => r.dirname(TypeMoq.It.isAny())) // Provide the target's parent. + .returns(() => ''); + const err = vscode.FileSystemError.FileNotFound('...'); + raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The parent dir exists. + .returns(() => Promise.reject(err)); + + const promise = filesystem.copyFile('spam', 'eggs'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.dirname(TypeMoq.It.isAny())) // Provide the target's parent. + .returns(() => ''); + raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The parent dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + raw.setup((r) => r.copy(TypeMoq.It.isAny(), TypeMoq.It.isAny(), { overwrite: true })) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.copyFile('spam', 'eggs'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); }); - test('Case sensitivity is not ignored when comparing file names on linux', async () => { - caseSensitivityFileCheck(false, false, true); + suite('rmFile', () => { + const opts = { + recursive: false, + useTrash: false, + }; + + test('wraps the low-level function', async () => { + const filename = 'x/y/z/spam.py'; + raw.setup((r) => r.delete(Uri(filename), opts)) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.rmfile(filename); + + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.delete(TypeMoq.It.isAny(), opts)) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.rmfile('spam.py'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); }); - test('Check existence of files synchronously', async () => { - expect(fileSystem.fileExistsSync(__filename)).to.be.equal(true, 'file not found'); + + suite('mkdirp', () => { + test('wraps the low-level function', async () => { + const dirname = 'x/y/z/spam'; + raw.setup((r) => r.createDirectory(Uri(dirname))) // expect the specific filename + .returns(() => Promise.resolve()); + + await filesystem.mkdirp(dirname); + + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.createDirectory(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.mkdirp('spam'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); }); - test('Test appending to file', async () => { - const dataToAppend = `Some Data\n${new Date().toString()}\nAnd another line`; - fileSystem.appendFileSync(fileToAppendTo, dataToAppend); - const fileContents = await fileSystem.readFile(fileToAppendTo); - expect(fileContents).to.be.equal(dataToAppend); + suite('rmdir', () => { + const opts = { + recursive: true, + useTrash: false, + }; + + test('directory is empty', async () => { + const dirname = 'x/y/z/spam'; + raw.setup((r) => r.readDirectory(Uri(dirname))) // The dir is empty. + .returns(() => Promise.resolve([])); + raw.setup((r) => r.delete(Uri(dirname), opts)) // Expect the specific args. + .returns(() => Promise.resolve()); + + await filesystem.rmdir(dirname); + + verifyAll(); + }); + + test('fails if readDirectory() fails (e.g. is a file)', async () => { + raw.setup((r) => r.readDirectory(TypeMoq.It.isAny())) // It's not a directory. + .throws(new Error('is a file')); + + const promise = filesystem.rmdir('spam'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + + test('fails if not empty', async () => { + const entries: [string, FileType][] = [ + ['dev1', FileType.Unknown], + ['w', FileType.Directory], + ['spam.py', FileType.File], + ['other', FileType.SymbolicLink | FileType.File], + ]; + raw.setup((r) => r.readDirectory(TypeMoq.It.isAny())) // The dir is not empty. + .returns(() => Promise.resolve(entries)); + + const promise = filesystem.rmdir('spam'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.readDirectory(TypeMoq.It.isAny())) // The "file" exists. + .returns(() => Promise.resolve([])); + raw.setup((r) => r.delete(TypeMoq.It.isAny(), opts)) // We don't care about the filename. + .throws(new Error('oops!')); + + const promise = filesystem.rmdir('spam'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); }); - test('Test searching for files', async () => { - const files = await fileSystem.search(path.join(__dirname, '*.js')); - expect(files).to.be.array(); - expect(files.length).to.be.at.least(1); - const expectedFileName = __filename.replace(/\\/g, '/'); - const fileName = files[0].replace(/\\/g, '/'); - expect(fileName).to.equal(expectedFileName); + + suite('rmtree', () => { + const opts = { + recursive: true, + useTrash: false, + }; + + test('wraps the low-level function', async () => { + const dirname = 'x/y/z/spam'; + raw.setup((r) => r.stat(Uri(dirname))) // The dir exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + raw.setup((r) => r.delete(Uri(dirname), opts)) // Expect the specific dirname. + .returns(() => Promise.resolve()); + + await filesystem.rmtree(dirname); + + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.stat(TypeMoq.It.isAny())) // The "file" exists. + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + raw.setup((r) => r.delete(TypeMoq.It.isAny(), opts)) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.rmtree('spam'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); }); - test('Ensure creating a temporary file results in a unique temp file path', async () => { - const tempFile = await fileSystem.createTemporaryFile('.tmp'); - const tempFile2 = await fileSystem.createTemporaryFile('.tmp'); - expect(tempFile.filePath).to.not.equal(tempFile2.filePath, 'Temp files must be unique, implementation of createTemporaryFile is off.'); + + suite('listdir', () => { + test('mixed', async () => { + const dirname = 'x/y/z/spam'; + const actual: [string, FileType][] = [ + ['dev1', FileType.Unknown], + ['w', FileType.Directory], + ['spam.py', FileType.File], + ['other', FileType.SymbolicLink | FileType.File], + ]; + const expected = actual.map(([basename, filetype]) => { + const filename = `x/y/z/spam/${basename}`; + raw.setup((r) => r.join(dirname, basename)) // Expect the specific basename. + .returns(() => filename); + return [filename, filetype] as [string, FileType]; + }); + raw.setup((r) => r.readDirectory(Uri(dirname))) // Expect the specific filename. + .returns(() => Promise.resolve(actual)); + + const entries = await filesystem.listdir(dirname); + + expect(entries).to.deep.equal(expected); + verifyAll(); + }); + + test('empty', async () => { + const dirname = 'x/y/z/spam'; + const expected: [string, FileType][] = []; + raw.setup((r) => r.readDirectory(Uri(dirname))) // expect the specific filename + .returns(() => Promise.resolve([])); + + const entries = await filesystem.listdir(dirname); + + expect(entries).to.deep.equal(expected); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.readDirectory(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + const promise = filesystem.listdir('spam'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); }); - test('Ensure writing to a temp file is supported via file stream', async () => { - await fileSystem.createTemporaryFile('.tmp').then((tf: TemporaryFile) => { - expect(tf).to.not.equal(undefined, 'Error trying to create a temporary file'); - const writeStream = fileSystem.createWriteStream(tf.filePath); - writeStream.write('hello', 'utf8', (err) => { - expect(err).to.equal(undefined, `Failed to write to a temp file, error is ${err}`); + + suite('statSync', () => { + test('wraps the low-level function (filetype: unknown)', async () => { + const filename = 'x/y/z/spam.py'; + const expected: FileStat = { + type: FileType.Unknown, + size: 10, + ctime: 101, + mtime: 102, + } as any; + const lstat = createMockLegacyStat(); + setupStatFileType(lstat, FileType.Unknown); + copyStat(expected, lstat); + raw.setup((r) => r.lstatSync(filename)) // expect the specific filename + .returns(() => lstat.object); + + const stat = filesystem.statSync(filename); + + expect(stat).to.deep.equal(expected); + verifyAll(); + }); + + [ + { kind: 'file', filetype: FileType.File }, + { kind: 'dir', filetype: FileType.Directory }, + ].forEach((testData) => { + test(`wraps the low-level function (filetype: ${testData.kind})`, async () => { + const filename = 'x/y/z/spam.py'; + const expected: FileStat = { + type: testData.filetype, + size: 10, + ctime: 101, + mtime: 102, + } as any; + const lstat = createMockLegacyStat(); + lstat + .setup((s) => s.isSymbolicLink()) // not a symlink + .returns(() => false); + setupStatFileType(lstat, testData.filetype); + copyStat(expected, lstat); + raw.setup((r) => r.lstatSync(filename)) // expect the specific filename + .returns(() => lstat.object); + + const stat = filesystem.statSync(filename); + + expect(stat).to.deep.equal(expected); + verifyAll(); + }); + }); + + [ + { kind: 'file', filetype: FileType.File }, + { kind: 'dir', filetype: FileType.Directory }, + { kind: 'unknown', filetype: FileType.Unknown }, + ].forEach((testData) => { + test(`wraps the low-level function (filetype: ${testData.kind} symlink)`, async () => { + const filename = 'x/y/z/spam.py'; + const expected: FileStat = { + type: testData.filetype | FileType.SymbolicLink, + size: 10, + ctime: 101, + mtime: 102, + } as any; + const lstat = createMockLegacyStat(); + lstat + .setup((s) => s.isSymbolicLink()) // not a symlink + .returns(() => true); + raw.setup((r) => r.lstatSync(filename)) // expect the specific filename + .returns(() => lstat.object); + const old = createMockLegacyStat(); + setupStatFileType(old, testData.filetype); + copyStat(expected, old); + raw.setup((r) => r.statSync(filename)) // expect the specific filename + .returns(() => old.object); + + const stat = filesystem.statSync(filename); + + expect(stat).to.deep.equal(expected); + verifyAll(); }); - }, (failReason) => { - expect(failReason).to.equal('No errors occured', `Failed to create a temporary file with error ${failReason}`); - }); - }); - test('Ensure chmod works against a temporary file', async () => { - await fileSystem.createTemporaryFile('.tmp').then(async (fl: TemporaryFile) => { - await fileSystem.chmod(fl.filePath, '7777').then( - (success: void) => { - // cannot check for success other than we got here, chmod in Windows won't have any effect on the file itself. - }, - (failReason) => { - expect(failReason).to.equal('There was no error using chmod', `Failed to perform chmod operation successfully, got error ${failReason}`); - }); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.lstatSync(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + expect(() => { + filesystem.statSync('spam.py'); + }).to.throw(); + verifyAll(); + }); + }); + + suite('readTextSync', () => { + test('wraps the low-level function', () => { + const filename = 'x/y/z/spam.py'; + const expected = '<text>'; + raw.setup((r) => r.readFileSync(filename, 'utf8')) // expect the specific filename + .returns(() => expected); + + const text = filesystem.readTextSync(filename); + + expect(text).to.equal(expected); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.readFileSync(TypeMoq.It.isAny(), TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + expect(() => filesystem.readTextSync('spam.py')).to.throw(); + + verifyAll(); + }); + }); + + suite('createReadStream', () => { + test('wraps the low-level function', () => { + const filename = 'x/y/z/spam.py'; + const expected = {} as any; + raw.setup((r) => r.createReadStream(filename)) // expect the specific filename + .returns(() => expected); + + const stream = filesystem.createReadStream(filename); + + expect(stream).to.equal(expected); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.createReadStream(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + expect(() => filesystem.createReadStream('spam.py')).to.throw(); + + verifyAll(); + }); + }); + + suite('createWriteStream', () => { + test('wraps the low-level function', () => { + const filename = 'x/y/z/spam.py'; + const expected = {} as any; + raw.setup((r) => r.createWriteStream(filename)) // expect the specific filename + .returns(() => expected); + + const stream = filesystem.createWriteStream(filename); + + expect(stream).to.equal(expected); + verifyAll(); + }); + + test('fails if the low-level call fails', async () => { + raw.setup((r) => r.createWriteStream(TypeMoq.It.isAny())) // We don't care about the filename. + .throws(new Error('file not found')); + + expect(() => filesystem.createWriteStream('spam.py')).to.throw(); + + verifyAll(); + }); + }); +}); + +interface IUtilsDeps extends IRawFileSystem, IFileSystemPaths, IFileSystemPathUtils, ITempFileSystem { + // helpers + getHash(data: string): string; + globFile(pat: string, options?: { cwd: string }): Promise<string[]>; +} + +suite('FileSystemUtils', () => { + let deps: TypeMoq.IMock<IUtilsDeps>; + let stats: TypeMoq.IMock<FileStat>[]; + let utils: FileSystemUtils; + setup(() => { + deps = TypeMoq.Mock.ofType<IUtilsDeps>(undefined, TypeMoq.MockBehavior.Strict); + + stats = []; + utils = new FileSystemUtils( + // Since it's a mock we can just use it for all 3 values. + deps.object, // rawFS + deps.object, // pathUtils + deps.object, // paths + deps.object, // tempFS + (data: string) => deps.object.getHash(data), + (pat: string, options?: { cwd: string }) => deps.object.globFile(pat, options), + ); + }); + function verifyAll() { + deps.verifyAll(); + stats.forEach((stat) => { + stat.verifyAll(); + }); + } + function createMockStat(): TypeMoq.IMock<FileStat> { + const stat = TypeMoq.Mock.ofType<FileStat>(undefined, TypeMoq.MockBehavior.Strict); + // This is necessary because passing "mock.object" to + // Promise.resolve() triggers the lookup. + stat.setup((s: any) => s.then) + .returns(() => undefined) + .verifiable(TypeMoq.Times.atLeast(0)); + stats.push(stat); + return stat; + } + + suite('createDirectory', () => { + test('wraps the low-level function', async () => { + const dirname = 'x/y/z/spam'; + deps.setup((d) => d.mkdirp(dirname)) // expect the specific filename + .returns(() => Promise.resolve()); + + await utils.createDirectory(dirname); + + verifyAll(); + }); + }); + + suite('deleteDirectory', () => { + test('wraps the low-level function', async () => { + const dirname = 'x/y/z/spam'; + deps.setup((d) => d.rmdir(dirname)) // expect the specific filename + .returns(() => Promise.resolve()); + + await utils.deleteDirectory(dirname); + + verifyAll(); + }); + }); + + suite('deleteFile', () => { + test('wraps the low-level function', async () => { + const filename = 'x/y/z/spam.py'; + deps.setup((d) => d.rmfile(filename)) // expect the specific filename + .returns(() => Promise.resolve()); + + await utils.deleteFile(filename); + + verifyAll(); + }); + }); + + suite('pathExists', () => { + test('exists (without type)', async () => { + const filename = 'x/y/z/spam.py'; + deps.setup((d) => d.pathExists(filename)) // The "file" exists. + .returns(() => Promise.resolve(true)); + + const exists = await utils.pathExists(filename); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('does not exist (without type)', async () => { + const filename = 'x/y/z/spam.py'; + deps.setup((d) => d.pathExists(filename)) // The "file" exists. + .returns(() => Promise.resolve(false)); + + const exists = await utils.pathExists(filename); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('matches (type: file)', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a file. + .returns(() => FileType.File); + deps.setup((d) => d.stat(filename)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(filename, FileType.File); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('mismatch (type: file)', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a directory. + .returns(() => FileType.Directory); + deps.setup((d) => d.stat(filename)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(filename, FileType.File); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('matches (type: directory)', async () => { + const dirname = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a directory. + .returns(() => FileType.Directory); + deps.setup((d) => d.stat(dirname)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(dirname, FileType.Directory); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('mismatch (type: directory)', async () => { + const dirname = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a file. + .returns(() => FileType.File); + deps.setup((d) => d.stat(dirname)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(dirname, FileType.Directory); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('symlinks are followed', async () => { + const symlink = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a symlink to a file. + .returns(() => FileType.File | FileType.SymbolicLink) + .verifiable(TypeMoq.Times.exactly(3)); + deps.setup((d) => d.stat(symlink)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)) + .verifiable(TypeMoq.Times.exactly(3)); + + const exists = await utils.pathExists(symlink, FileType.SymbolicLink); + const destIsFile = await utils.pathExists(symlink, FileType.File); + const destIsDir = await utils.pathExists(symlink, FileType.Directory); + + expect(exists).to.equal(true); + expect(destIsFile).to.equal(true); + expect(destIsDir).to.equal(false); + verifyAll(); + }); + + test('mismatch (type: symlink)', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a file. + .returns(() => FileType.File); + deps.setup((d) => d.stat(filename)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(filename, FileType.SymbolicLink); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('matches (type: unknown)', async () => { + const sockFile = 'x/y/z/ipc.sock'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a socket. + .returns(() => FileType.Unknown); + deps.setup((d) => d.stat(sockFile)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(sockFile, FileType.Unknown); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('mismatch (type: unknown)', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a file. + .returns(() => FileType.File); + deps.setup((d) => d.stat(filename)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.pathExists(filename, FileType.Unknown); + + expect(exists).to.equal(false); + verifyAll(); + }); + }); + + suite('fileExists', () => { + test('want file, got file', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a File. + .returns(() => FileType.File); + deps.setup((d) => d.stat(filename)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.fileExists(filename); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('want file, not file', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a directory. + .returns(() => FileType.Directory); + deps.setup((d) => d.stat(filename)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.fileExists(filename); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('symlink', async () => { + const symlink = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a symlink to a File. + .returns(() => FileType.File | FileType.SymbolicLink); + deps.setup((d) => d.stat(symlink)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.fileExists(symlink); + + // This is because we currently use stat() and not lstat(). + expect(exists).to.equal(true); + verifyAll(); + }); + + test('unknown', async () => { + const sockFile = 'x/y/z/ipc.sock'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a socket. + .returns(() => FileType.Unknown); + deps.setup((d) => d.stat(sockFile)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.fileExists(sockFile); + + expect(exists).to.equal(false); + verifyAll(); + }); + }); + + suite('directoryExists', () => { + test('want directory, got directory', async () => { + const dirname = 'x/y/z/spam'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a directory. + .returns(() => FileType.Directory); + deps.setup((d) => d.stat(dirname)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.directoryExists(dirname); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('want directory, not directory', async () => { + const dirname = 'x/y/z/spam'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a file. + .returns(() => FileType.File); + deps.setup((d) => d.stat(dirname)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.directoryExists(dirname); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('symlink', async () => { + const symlink = 'x/y/z/spam'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a symlink to a directory. + .returns(() => FileType.Directory | FileType.SymbolicLink); + deps.setup((d) => d.stat(symlink)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.directoryExists(symlink); + + // This is because we currently use stat() and not lstat(). + expect(exists).to.equal(true); + verifyAll(); + }); + + test('unknown', async () => { + const sockFile = 'x/y/z/ipc.sock'; + const stat = createMockStat(); + stat.setup((s) => s.type) // It's a socket. + .returns(() => FileType.Unknown); + deps.setup((d) => d.stat(sockFile)) // The "file" exists. + .returns(() => Promise.resolve(stat.object)); + + const exists = await utils.directoryExists(sockFile); + + expect(exists).to.equal(false); + verifyAll(); + }); + }); + + suite('listdir', () => { + test('wraps the raw call on success', async () => { + const dirname = 'x/y/z/spam'; + const expected: [string, FileType][] = [ + ['x/y/z/spam/dev1', FileType.Unknown], + ['x/y/z/spam/w', FileType.Directory], + ['x/y/z/spam/spam.py', FileType.File], + ['x/y/z/spam/other', FileType.SymbolicLink | FileType.File], + ]; + deps.setup((d) => d.listdir(dirname)) // Full results get returned from RawFileSystem.listdir(). + .returns(() => Promise.resolve(expected)); + + const entries = await utils.listdir(dirname); + + expect(entries).to.deep.equal(expected); + verifyAll(); + }); + + test('returns [] if the directory does not exist', async () => { + const dirname = 'x/y/z/spam'; + const err = vscode.FileSystemError.FileNotFound(dirname); + deps.setup((d) => d.listdir(dirname)) // The "file" does not exist. + .returns(() => Promise.reject(err)); + deps.setup((d) => d.pathExists(dirname)) // The "file" does not exist. + .returns(() => Promise.resolve(false)); + + const entries = await utils.listdir(dirname); + + expect(entries).to.deep.equal([]); + verifyAll(); + }); + + test('fails if not a directory', async () => { + const dirname = 'x/y/z/spam'; + const err = vscode.FileSystemError.FileNotADirectory(dirname); + deps.setup((d) => d.listdir(dirname)) // Fail (async) with not-a-directory. + .returns(() => Promise.reject(err)); + deps.setup((d) => d.pathExists(dirname)).returns(() => Promise.resolve(true)); // The "file" exists. + + const promise = utils.listdir(dirname); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + + test('fails if the raw call promise fails', async () => { + const dirname = 'x/y/z/spam'; + const err = new Error('oops!'); + deps.setup((d) => d.listdir(dirname)) // Fail (async) with an arbitrary error. + .returns(() => Promise.reject(err)); + deps.setup((d) => d.pathExists(dirname)).returns(() => Promise.resolve(false)); + + const entries = await utils.listdir(dirname); + + expect(entries).to.deep.equal([]); + verifyAll(); + }); + }); + + suite('getSubDirectories', () => { + test('filters out non-subdirs', async () => { + const dirname = 'x/y/z/spam'; + const entries: [string, FileType][] = [ + ['x/y/z/spam/dev1', FileType.Unknown], + ['x/y/z/spam/w', FileType.Directory], + ['x/y/z/spam/spam.py', FileType.File], + ['x/y/z/spam/v', FileType.Directory], + ['x/y/z/spam/eggs.py', FileType.File], + ['x/y/z/spam/other1', FileType.SymbolicLink | FileType.File], + ['x/y/z/spam/other2', FileType.SymbolicLink | FileType.Directory], + ]; + const expected = [ + // only entries with FileType.Directory + 'x/y/z/spam/w', + 'x/y/z/spam/v', + 'x/y/z/spam/other2', + ]; + deps.setup((d) => d.listdir(dirname)) // Full results get returned from RawFileSystem.listdir(). + .returns(() => Promise.resolve(entries)); + + const filtered = await utils.getSubDirectories(dirname); + + expect(filtered).to.deep.equal(expected); + verifyAll(); + }); + }); + + suite('getFiles', () => { + test('filters out non-files', async () => { + const filename = 'x/y/z/spam'; + const entries: [string, FileType][] = [ + ['x/y/z/spam/dev1', FileType.Unknown], + ['x/y/z/spam/w', FileType.Directory], + ['x/y/z/spam/spam.py', FileType.File], + ['x/y/z/spam/v', FileType.Directory], + ['x/y/z/spam/eggs.py', FileType.File], + ['x/y/z/spam/other1', FileType.SymbolicLink | FileType.File], + ['x/y/z/spam/other2', FileType.SymbolicLink | FileType.Directory], + ]; + const expected = [ + // only entries with FileType.File + 'x/y/z/spam/spam.py', + 'x/y/z/spam/eggs.py', + 'x/y/z/spam/other1', + ]; + deps.setup((d) => d.listdir(filename)) // Full results get returned from RawFileSystem.listdir(). + .returns(() => Promise.resolve(entries)); + + const filtered = await utils.getFiles(filename); + + expect(filtered).to.deep.equal(expected); + verifyAll(); + }); + }); + + suite('isDirReadonly', () => { + setup(() => { + deps.setup((d) => d.sep) // The value really doesn't matter. + .returns(() => '/'); + }); + + test('is not readonly', async () => { + const dirname = 'x/y/z/spam'; + const filename = `${dirname}/___vscpTest___`; + deps.setup((d) => d.stat(dirname)) // Success! + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + deps.setup((d) => d.writeText(filename, '')) // Success! + .returns(() => Promise.resolve()); + deps.setup((d) => d.rmfile(filename)) // Success! + .returns(() => Promise.resolve()); + + const isReadonly = await utils.isDirReadonly(dirname); + + expect(isReadonly).to.equal(false); + verifyAll(); + }); + + test('is readonly', async () => { + const dirname = 'x/y/z/spam'; + const filename = `${dirname}/___vscpTest___`; + const err = new Error('not permitted'); + + (err as any).code = 'EACCES'; // errno + deps.setup((d) => d.stat(dirname)) // Success! + .returns(() => Promise.resolve((undefined as unknown) as FileStat)); + deps.setup((d) => d.writeText(filename, '')) // not permitted + .returns(() => Promise.reject(err)); + + const isReadonly = await utils.isDirReadonly(dirname); + + expect(isReadonly).to.equal(true); + verifyAll(); + }); + + test('fails if the directory does not exist', async () => { + const dirname = 'x/y/z/spam'; + const err = new Error('not found'); + + (err as any).code = 'ENOENT'; // errno + deps.setup((d) => d.stat(dirname)) // file-not-found + .returns(() => Promise.reject(err)); + + const promise = utils.isDirReadonly(dirname); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('getFileHash', () => { + test('Getting hash for a file should return non-empty string', async () => { + const filename = 'x/y/z/spam.py'; + const stat = createMockStat(); + stat.setup((s) => s.ctime) // created + .returns(() => 100); + stat.setup((s) => s.mtime) // modified + .returns(() => 120); + deps.setup((d) => d.lstat(filename)) // file exists + .returns(() => Promise.resolve(stat.object)); + deps.setup((d) => d.getHash('100-120')) // built from ctime and mtime + .returns(() => 'deadbeef'); + + const hash = await utils.getFileHash(filename); + + expect(hash).to.equal('deadbeef'); + verifyAll(); + }); + + test('Getting hash for non existent file should throw error', async () => { + const filename = 'x/y/z/spam.py'; + const err = vscode.FileSystemError.FileNotFound(filename); + deps.setup((d) => d.lstat(filename)) // file-not-found + .returns(() => Promise.reject(err)); + + const promise = utils.getFileHash(filename); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); + + suite('search', () => { + test('found matches (without cwd)', async () => { + const pattern = `x/y/z/spam.*`; + const expected: string[] = [ + // We can pretend that there were other files + // that were ignored. + 'x/y/z/spam.py', + 'x/y/z/spam.pyc', + 'x/y/z/spam.so', + 'x/y/z/spam.data', + ]; + deps.setup((d) => d.globFile(pattern, undefined)) // found some + .returns(() => Promise.resolve(expected)); + + const files = await utils.search(pattern); + + expect(files).to.deep.equal(expected); + verifyAll(); + }); + + test('found matches (with cwd)', async () => { + const pattern = `x/y/z/spam.*`; + const cwd = 'a/b/c'; + const expected: string[] = [ + // We can pretend that there were other files + // that were ignored. + 'x/y/z/spam.py', + 'x/y/z/spam.pyc', + 'x/y/z/spam.so', + 'x/y/z/spam.data', + ]; + deps.setup((d) => d.globFile(pattern, { cwd: cwd })) // found some + .returns(() => Promise.resolve(expected)); + + const files = await utils.search(pattern, cwd); + + expect(files).to.deep.equal(expected); + verifyAll(); + }); + + test('no matches (empty)', async () => { + const pattern = `x/y/z/spam.*`; + deps.setup((d) => d.globFile(pattern, undefined)) // found none + .returns(() => Promise.resolve([])); + + const files = await utils.search(pattern); + + expect(files).to.deep.equal([]); + verifyAll(); + }); + + test('no matches (undefined)', async () => { + const pattern = `x/y/z/spam.*`; + deps.setup((d) => d.globFile(pattern, undefined)) // found none + .returns(() => Promise.resolve((undefined as unknown) as string[])); + + const files = await utils.search(pattern); + + expect(files).to.deep.equal([]); + verifyAll(); + }); + }); + + suite('fileExistsSync', () => { + test('file exists', async () => { + const filename = 'x/y/z/spam.py'; + deps.setup((d) => d.statSync(filename)) // The file exists. + .returns(() => (undefined as unknown) as FileStat); + + const exists = utils.fileExistsSync(filename); + + expect(exists).to.equal(true); + verifyAll(); + }); + + test('file does not exist', async () => { + const filename = 'x/y/z/spam.py'; + const err = vscode.FileSystemError.FileNotFound('...'); + deps.setup((d) => d.statSync(filename)) // The file does not exist. + .throws(err); + + const exists = utils.fileExistsSync(filename); + + expect(exists).to.equal(false); + verifyAll(); + }); + + test('fails if low-level call fails', async () => { + const filename = 'x/y/z/spam.py'; + const err = new Error('oops!'); + deps.setup((d) => d.statSync(filename)) // big badda boom + .throws(err); + + expect(() => utils.fileExistsSync(filename)).to.throw(err); + verifyAll(); }); }); }); diff --git a/src/test/common/platform/fs-paths.functional.test.ts b/src/test/common/platform/fs-paths.functional.test.ts new file mode 100644 index 000000000000..a7e6bfd0559d --- /dev/null +++ b/src/test/common/platform/fs-paths.functional.test.ts @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as os from 'os'; +import * as path from 'path'; +import { Executables, FileSystemPaths, FileSystemPathUtils } from '../../../client/common/platform/fs-paths'; +import { WINDOWS as IS_WINDOWS } from './utils'; + +suite('FileSystem - Paths', () => { + let paths: FileSystemPaths; + setup(() => { + paths = FileSystemPaths.withDefaults(); + }); + + suite('separator', () => { + test('matches node', () => { + expect(paths.sep).to.be.equal(path.sep); + }); + }); + + suite('dirname', () => { + test('with dirname', () => { + const filename = path.join('spam', 'eggs', 'spam.py'); + const expected = path.join('spam', 'eggs'); + + const basename = paths.dirname(filename); + + expect(basename).to.equal(expected); + }); + + test('without dirname', () => { + const filename = 'spam.py'; + const expected = '.'; + + const basename = paths.dirname(filename); + + expect(basename).to.equal(expected); + }); + }); + + suite('basename', () => { + test('with dirname', () => { + const filename = path.join('spam', 'eggs', 'spam.py'); + const expected = 'spam.py'; + + const basename = paths.basename(filename); + + expect(basename).to.equal(expected); + }); + + test('without dirname', () => { + const filename = 'spam.py'; + const expected = filename; + + const basename = paths.basename(filename); + + expect(basename).to.equal(expected); + }); + }); + + suite('normalize', () => { + test('noop', () => { + const filename = path.join('spam', 'eggs', 'spam.py'); + const expected = filename; + + const norm = paths.normalize(filename); + + expect(norm).to.equal(expected); + }); + + test('pathological', () => { + const filename = path.join(path.sep, 'spam', '..', 'eggs', '.', 'spam.py'); + const expected = path.join(path.sep, 'eggs', 'spam.py'); + + const norm = paths.normalize(filename); + + expect(norm).to.equal(expected); + }); + + test('relative to CWD', () => { + const filename = path.join('..', 'spam', 'eggs', 'spam.py'); + const expected = filename; + + const norm = paths.normalize(filename); + + expect(norm).to.equal(expected); + }); + + test('parent of root fails', () => { + const filename = path.join(path.sep, '..'); + const expected = filename; + + const norm = paths.normalize(filename); + + expect(norm).to.equal(expected); + }); + }); + + suite('join', () => { + test('parts get joined by path.sep', () => { + const expected = path.join('x', 'y', 'z', 'spam.py'); + + const result = paths.join( + 'x', + // Be explicit here to ensure our assumptions are correct + // about the relationship between "sep" and "join()". + path.sep === '\\' ? 'y\\z' : 'y/z', + 'spam.py', + ); + + expect(result).to.equal(expected); + }); + }); + + suite('normCase', () => { + test('forward-slash', () => { + const filename = 'X/Y/Z/SPAM.PY'; + const expected = IS_WINDOWS ? 'X\\Y\\Z\\SPAM.PY' : filename; + + const result = paths.normCase(filename); + + expect(result).to.equal(expected); + }); + + test('backslash is not changed', () => { + const filename = 'X\\Y\\Z\\SPAM.PY'; + const expected = filename; + + const result = paths.normCase(filename); + + expect(result).to.equal(expected); + }); + + test('lower-case', () => { + const filename = 'x\\y\\z\\spam.py'; + const expected = IS_WINDOWS ? 'X\\Y\\Z\\SPAM.PY' : filename; + + const result = paths.normCase(filename); + + expect(result).to.equal(expected); + }); + + test('upper-case stays upper-case', () => { + const filename = 'X\\Y\\Z\\SPAM.PY'; + const expected = 'X\\Y\\Z\\SPAM.PY'; + + const result = paths.normCase(filename); + + expect(result).to.equal(expected); + }); + }); +}); + +suite('FileSystem - Executables', () => { + let execs: Executables; + setup(() => { + execs = Executables.withDefaults(); + }); + + suite('delimiter', () => { + test('matches node', () => { + expect(execs.delimiter).to.be.equal(path.delimiter); + }); + }); + + suite('getPathVariableName', () => { + const expected = IS_WINDOWS ? 'Path' : 'PATH'; + + test('matches platform', () => { + expect(execs.envVar).to.equal(expected); + }); + }); +}); + +suite('FileSystem - Path Utils', () => { + let utils: FileSystemPathUtils; + setup(() => { + utils = FileSystemPathUtils.withDefaults(); + }); + + suite('arePathsSame', () => { + test('identical', () => { + const filename = 'x/y/z/spam.py'; + + const result = utils.arePathsSame(filename, filename); + + expect(result).to.equal(true); + }); + + test('not the same', () => { + const file1 = 'x/y/z/spam.py'; + const file2 = 'a/b/c/spam.py'; + + const result = utils.arePathsSame(file1, file2); + + expect(result).to.equal(false); + }); + + test('with different separators', () => { + const file1 = 'x/y/z/spam.py'; + const file2 = 'x\\y\\z\\spam.py'; + const expected = IS_WINDOWS; + + const result = utils.arePathsSame(file1, file2); + + expect(result).to.equal(expected); + }); + + test('with different case', () => { + const file1 = 'x/y/z/spam.py'; + const file2 = 'x/Y/z/Spam.py'; + const expected = IS_WINDOWS; + + const result = utils.arePathsSame(file1, file2); + + expect(result).to.equal(expected); + }); + }); + + suite('getDisplayName', () => { + const relname = path.join('spam', 'eggs', 'spam.py'); + const cwd = path.resolve(path.sep, 'x', 'y', 'z'); + + test('filename matches CWD', () => { + const filename = path.join(cwd, relname); + const expected = `.${path.sep}${relname}`; + + const display = utils.getDisplayName(filename, cwd); + + expect(display).to.equal(expected); + }); + + test('filename does not match CWD', () => { + const filename = path.resolve(cwd, '..', relname); + const expected = filename; + + const display = utils.getDisplayName(filename, cwd); + + expect(display).to.equal(expected); + }); + + test('filename matches home dir, not cwd', () => { + const filename = path.join(os.homedir(), relname); + const expected = path.join('~', relname); + + const display = utils.getDisplayName(filename, cwd); + + expect(display).to.equal(expected); + }); + + test('filename matches home dir', () => { + const filename = path.join(os.homedir(), relname); + const expected = path.join('~', relname); + + const display = utils.getDisplayName(filename); + + expect(display).to.equal(expected); + }); + + test('filename does not match home dir', () => { + const filename = relname; + const expected = filename; + + const display = utils.getDisplayName(filename); + + expect(display).to.equal(expected); + }); + }); +}); diff --git a/src/test/common/platform/fs-paths.unit.test.ts b/src/test/common/platform/fs-paths.unit.test.ts new file mode 100644 index 000000000000..b34b65d01e53 --- /dev/null +++ b/src/test/common/platform/fs-paths.unit.test.ts @@ -0,0 +1,114 @@ +// 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 { FileSystemPathUtils } from '../../../client/common/platform/fs-paths'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { OSType } from '../../../client/common/utils/platform'; + +interface IUtilsDeps { + // executables + delimiter: string; + envVar: string; + // paths + readonly sep: string; + join(...filenames: string[]): string; + dirname(filename: string): string; + basename(filename: string, suffix?: string): string; + normalize(filename: string): string; + normCase(filename: string): string; + // node "path" + relative(relpath: string, rootpath: string): string; +} + +suite('FileSystem - Path Utils', () => { + let deps: TypeMoq.IMock<IUtilsDeps>; + let utils: FileSystemPathUtils; + setup(() => { + deps = TypeMoq.Mock.ofType<IUtilsDeps>(undefined, TypeMoq.MockBehavior.Strict); + utils = new FileSystemPathUtils( + 'my-home', + // It's simpler to just use one mock for all 3 dependencies. + deps.object, + deps.object, + deps.object, + ); + }); + function verifyAll() { + deps.verifyAll(); + } + + suite('path-related', () => { + const caseInsensitive = [OSType.Windows]; + + suite('arePathsSame', () => { + getNamesAndValues<OSType>(OSType).forEach((item) => { + const osType = item.value; + + function setNormCase(filename: string, numCalls = 1): string { + let norm = filename; + if (osType === OSType.Windows) { + norm = path.normalize(filename).toUpperCase(); + } + deps.setup((d) => d.normCase(filename)) + .returns(() => norm) + .verifiable(TypeMoq.Times.exactly(numCalls)); + return filename; + } + + [ + // no upper-case + 'c:\\users\\peter smith\\my documents\\test.txt', + // some upper-case + 'c:\\USERS\\Peter Smith\\my documents\\test.TXT', + ].forEach((path1) => { + test(`True if paths are identical (type: ${item.name}) - ${path1}`, () => { + path1 = setNormCase(path1, 2); + + const areSame = utils.arePathsSame(path1, path1); + + expect(areSame).to.be.equal(true, 'file paths do not match'); + verifyAll(); + }); + }); + + test(`False if paths are completely different (type: ${item.name})`, () => { + const path1 = setNormCase('c:\\users\\Peter Smith\\my documents\\test.txt'); + const path2 = setNormCase('c:\\users\\Peter Smith\\my documents\\test.exe'); + + const areSame = utils.arePathsSame(path1, path2); + + expect(areSame).to.be.equal(false, 'file paths do not match'); + verifyAll(); + }); + + if (caseInsensitive.includes(osType)) { + test(`True if paths only differ by case (type: ${item.name})`, () => { + const path1 = setNormCase('c:\\users\\Peter Smith\\my documents\\test.txt'); + const path2 = setNormCase('c:\\USERS\\Peter Smith\\my documents\\test.TXT'); + + const areSame = utils.arePathsSame(path1, path2); + + expect(areSame).to.be.equal(true, 'file paths match'); + verifyAll(); + }); + } else { + test(`False if paths only differ by case (type: ${item.name})`, () => { + const path1 = setNormCase('c:\\users\\Peter Smith\\my documents\\test.txt'); + const path2 = setNormCase('c:\\USERS\\Peter Smith\\my documents\\test.TXT'); + + const areSame = utils.arePathsSame(path1, path2); + + expect(areSame).to.be.equal(false, 'file paths do not match'); + verifyAll(); + }); + } + + // Missing tests: + // * exercize normalization + }); + }); + }); +}); diff --git a/src/test/common/platform/fs-temp.functional.test.ts b/src/test/common/platform/fs-temp.functional.test.ts new file mode 100644 index 000000000000..67bca3338e76 --- /dev/null +++ b/src/test/common/platform/fs-temp.functional.test.ts @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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 } from './utils'; + +const assertArrays = require('chai-arrays'); +use(require('chai-as-promised')); +use(assertArrays); + +suite('FileSystem - TemporaryFileSystem', () => { + let tmpfs: TemporaryFileSystem; + let fix: FSFixture; + setup(async () => { + tmpfs = TemporaryFileSystem.withDefaults(); + fix = new FSFixture(); + }); + teardown(async () => { + await fix.cleanUp(); + }); + + suite('createFile', () => { + async function createFile(suffix: string): Promise<TemporaryFile> { + const tempfile = await tmpfs.createFile(suffix); + fix.addFSCleanup(tempfile.filePath, tempfile.dispose); + return tempfile; + } + + test('TemporaryFile is created properly', async () => { + const tempfile = await tmpfs.createFile('.tmp'); + fix.addFSCleanup(tempfile.filePath, tempfile.dispose); + await assertExists(tempfile.filePath); + + expect(tempfile.filePath.endsWith('.tmp')).to.equal(true, `bad suffix on ${tempfile.filePath}`); + }); + + test('TemporaryFile is disposed properly', async () => { + const tempfile = await createFile('.tmp'); + await assertExists(tempfile.filePath); + + tempfile.dispose(); + + await assertDoesNotExist(tempfile.filePath); + }); + + test('Ensure creating a temporary file results in a unique temp file path', async () => { + const tempFile = await createFile('.tmp'); + const tempFile2 = await createFile('.tmp'); + + const filename1 = tempFile.filePath; + const filename2 = tempFile2.filePath; + + expect(filename1).to.not.equal(filename2); + }); + + test('Ensure chmod works against a temporary file', async () => { + // Note that on Windows chmod is a noop. + const tempfile = await createFile('.tmp'); + + const promise = fs.chmod(tempfile.filePath, '7777'); + + await expect(promise).to.not.eventually.be.rejected; + }); + }); +}); diff --git a/src/test/common/platform/fs-temp.unit.test.ts b/src/test/common/platform/fs-temp.unit.test.ts new file mode 100644 index 000000000000..29b4e5f42b12 --- /dev/null +++ b/src/test/common/platform/fs-temp.unit.test.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { TemporaryFileSystem } from '../../../client/common/platform/fs-temp'; + +interface IDeps { + // tmp module + fileSync(config: { + postfix?: string; + mode?: number; + }): { + name: string; + fd: number; + removeCallback(): void; + }; +} + +suite('FileSystem - temp files', () => { + let deps: TypeMoq.IMock<IDeps>; + let temp: TemporaryFileSystem; + setup(() => { + deps = TypeMoq.Mock.ofType<IDeps>(undefined, TypeMoq.MockBehavior.Strict); + temp = new TemporaryFileSystem(deps.object); + }); + function verifyAll() { + deps.verifyAll(); + } + + suite('createFile', () => { + test(`fails if the raw call fails`, async () => { + const failure = new Error('oops'); + deps.setup((d) => d.fileSync({ postfix: '.tmp', mode: undefined })) + // fail with an arbitrary error + .throws(failure); + + const promise = temp.createFile('.tmp'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + + test(`fails if the raw call "returns" an error`, async () => { + const failure = new Error('oops'); + deps.setup((d) => d.fileSync({ postfix: '.tmp', mode: undefined })).callback((_cfg, cb) => + cb(failure, '...', -1, () => {}), + ); + + const promise = temp.createFile('.tmp'); + + await expect(promise).to.eventually.be.rejected; + verifyAll(); + }); + }); +}); diff --git a/src/test/common/platform/pathUtils.functional.test.ts b/src/test/common/platform/pathUtils.functional.test.ts new file mode 100644 index 000000000000..35938f687b3b --- /dev/null +++ b/src/test/common/platform/pathUtils.functional.test.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { FileSystemPathUtils } from '../../../client/common/platform/fs-paths'; +import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { WINDOWS as IS_WINDOWS } from './utils'; + +suite('FileSystem - PathUtils', () => { + let utils: PathUtils; + let wrapped: FileSystemPathUtils; + setup(() => { + utils = new PathUtils(IS_WINDOWS); + wrapped = FileSystemPathUtils.withDefaults(); + }); + + suite('home', () => { + test('matches wrapped object', () => { + const expected = wrapped.home; + + expect(utils.home).to.equal(expected); + }); + }); + + suite('delimiter', () => { + test('matches wrapped object', () => { + const expected = wrapped.executables.delimiter; + + expect(utils.delimiter).to.be.equal(expected); + }); + }); + + suite('separator', () => { + test('matches wrapped object', () => { + const expected = wrapped.paths.sep; + + expect(utils.separator).to.be.equal(expected); + }); + }); + + suite('getPathVariableName', () => { + test('matches wrapped object', () => { + const expected = wrapped.executables.envVar; + + const envVar = utils.getPathVariableName(); + + expect(envVar).to.equal(expected); + }); + }); + + suite('getDisplayName', () => { + test('matches wrapped object', () => { + const filename = 'spam.py'; + const expected = wrapped.getDisplayName(filename); + + const display = utils.getDisplayName(filename); + + expect(display).to.equal(expected); + }); + }); + + suite('basename', () => { + test('matches wrapped object', () => { + const filename = 'spam.py'; + const expected = wrapped.paths.basename(filename); + + const basename = utils.basename(filename); + + expect(basename).to.equal(expected); + }); + }); +}); diff --git a/src/test/common/platform/pathUtils.test.ts b/src/test/common/platform/pathUtils.test.ts deleted file mode 100644 index f8f9d2d32597..000000000000 --- a/src/test/common/platform/pathUtils.test.ts +++ /dev/null @@ -1,18 +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 { PathUtils } from '../../../client/common/platform/pathUtils'; -import { getOSType, OSType } from '../../common'; - -suite('PathUtils', () => { - let utils: PathUtils; - suiteSetup(() => { - utils = new PathUtils(getOSType() === OSType.Windows); - }); - test('Path Separator', () => { - expect(utils.separator).to.be.equal(path.sep); - }); -}); 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 6bc195f35033..000000000000 --- a/src/test/common/platform/platformService.test.ts +++ /dev/null @@ -1,101 +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('is64bit', async () => { - // tslint:disable-next-line:no-require-imports - const arch = require('arch') as typeof import('arch'); - - const hostReports64Bit = arch() === 'x64'; - const svc = new PlatformService(); - const result = svc.is64bit; - - expect(result).to.be.equal(hostReports64Bit, `arch() reports '${arch()}', PlatformService.is64bit reports ${result}.`); - }); - - test('getVersion on Mac/Windows', async function () { - if (osType === OSType.Linux) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - const expectedVersion = parse(os.release())!; - const svc = new PlatformService(); - const result = await svc.getVersion(); - - expect(result.compare(expectedVersion)).to.be.equal(0, 'invalid value'); - }); - test('getVersion on Linux shoud throw an exception', async function () { - if (osType !== OSType.Linux) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - const svc = new PlatformService(); - - await expect(svc.getVersion()).to.eventually.be.rejectedWith('Not Supported'); - }); -}); - -function getOSType(platform: string = process.platform): OSType { - if (/^win/.test(platform)) { - return OSType.Windows; - } else if (/^darwin/.test(platform)) { - return OSType.OSX; - } else if (/^linux/.test(platform)) { - return OSType.Linux; - } else { - return OSType.Unknown; - } -} diff --git a/src/test/common/platform/serviceRegistry.unit.test.ts b/src/test/common/platform/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..109a633e0489 --- /dev/null +++ b/src/test/common/platform/serviceRegistry.unit.test.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { RegistryImplementation } from '../../../client/common/platform/registry'; +import { registerTypes } from '../../../client/common/platform/serviceRegistry'; +import { IFileSystem, IPlatformService, IRegistry } from '../../../client/common/platform/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; + +suite('Common Platform Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager)); + verify(serviceManager.addSingleton<IPlatformService>(IPlatformService, PlatformService)).once(); + verify(serviceManager.addSingleton<IFileSystem>(IFileSystem, FileSystem)).once(); + verify(serviceManager.addSingleton<IRegistry>(IRegistry, RegistryImplementation)).once(); + }); +}); diff --git a/src/test/common/platform/utils.ts b/src/test/common/platform/utils.ts new file mode 100644 index 000000000000..881e3cd019b9 --- /dev/null +++ b/src/test/common/platform/utils.ts @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +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. + +export const WINDOWS = /^win/.test(process.platform); +export const OSX = /^darwin/.test(process.platform); + +export const SUPPORTS_SYMLINKS = (() => { + const source = fsextra.readdirSync('.')[0]; + const symlink = `${source}.symlink`; + try { + fsextra.symlinkSync(source, symlink); + } catch { + return false; + } + fsextra.unlinkSync(symlink); + return true; +})(); +export const SUPPORTS_SOCKETS = (() => { + if (WINDOWS) { + // Windows requires named pipes to have a specific path under + // the local domain ("\\.\pipe\*"). This makes them relatively + // useless in our functional tests, where we want to use them + // to exercise FileType.Unknown. + return false; + } + const tmp = tmpMod.dirSync({ + prefix: 'pyvsc-test-', + unsafeCleanup: true, // for non-empty dir + }); + const filename = path.join(tmp.name, 'test.sock'); + try { + const srv = net.createServer(); + try { + srv.listen(filename); + } finally { + srv.close(); + } + } catch { + return false; + } finally { + tmp.removeCallback(); + } + return true; +})(); + +export const DOES_NOT_EXIST = 'this file does not exist'; + +export async function assertDoesNotExist(filename: string) { + const promise = fsextra.stat(filename); + await expect(promise).to.eventually.be.rejected; +} + +export async function assertExists(filename: string) { + const promise = fsextra.stat(filename); + await expect(promise).to.not.eventually.be.rejected; +} + +export async function assertFileText(filename: string, expected: string): Promise<string> { + const data = await fsextra.readFile(filename); + const text = data.toString(); + expect(text).to.equal(expected); + return text; +} + +export function fixPath(filename: string): string { + return path.normalize(filename); +} + +export class SystemError extends Error { + public code: string; + public errno: number; + public syscall: string; + public info?: string; + public path?: string; + public address?: string; + public dest?: string; + public port?: string; + constructor(code: string, syscall: string, message: string) { + super(`${code}: ${message} ${syscall} '...'`); + this.code = code; + this.errno = 0; // Don't bother until we actually need it. + this.syscall = syscall; + } +} + +export class FSFixture extends CleanupFixture { + private tempDir: string | undefined; + private sockServer: net.Server | undefined; + + public addFSCleanup(filename: string, dispose?: () => void) { + this.addCleanup(() => this.ensureDeleted(filename, dispose)); + } + + public async resolve(relname: string, mkdirs = true): Promise<string> { + const tempDir = this.ensureTempDir(); + relname = path.normalize(relname); + const filename = path.join(tempDir, relname); + if (mkdirs) { + const dirname = path.dirname(filename); + await fsextra.mkdirp(dirname); + } + return filename; + } + + public async createFile(relname: string, text = ''): Promise<string> { + const filename = await this.resolve(relname); + await fsextra.writeFile(filename, text); + return filename; + } + + public async createDirectory(relname: string): Promise<string> { + const dirname = await this.resolve(relname); + await fsextra.mkdir(dirname); + return dirname; + } + + public async createSymlink(relname: string, source: string): Promise<string> { + if (!SUPPORTS_SYMLINKS) { + throw Error('this platform does not support symlinks'); + } + const symlink = await this.resolve(relname); + // We cannot use fsextra.ensureSymlink() because it requires + // that "source" exist. + await fsextra.symlink(source, symlink); + return symlink; + } + + public async createSocket(relname: string): Promise<string> { + const srv = this.ensureSocketServer(); + const filename = await this.resolve(relname); + await new Promise<void>((resolve) => srv!.listen(filename, 0, resolve)); + return filename; + } + + public async ensureDeleted(filename: string, dispose?: () => void) { + if (dispose) { + try { + dispose(); + return; // Trust that dispose() did what it's supposed to. + } catch (err) { + // For temp directories, the "unsafeCleanup: true" + // option of the "tmp" module is supposed to support + // a non-empty directory, but apparently that isn't + // always the case. + // (see #8804) + if (!(await fsextra.pathExists(filename))) { + return; + } + console.log(`failure during dispose() for ${filename}: ${err}`); + console.log('...manually deleting'); + // Fall back to fsextra. + } + } + + try { + await fsextra.remove(filename); + } catch (err) { + console.log(`failure while deleting ${filename}: ${err}`); + } + } + + private ensureTempDir(): string { + if (this.tempDir) { + return this.tempDir; + } + + const tempDir = tmpMod.dirSync({ + prefix: 'pyvsc-fs-tests-', + unsafeCleanup: true, + }); + this.tempDir = tempDir.name; + + this.addFSCleanup(tempDir.name, async () => { + if (!this.tempDir) { + return; + } + this.tempDir = undefined; + + await this.ensureDeleted(tempDir.name, tempDir.removeCallback); + //try { + // tempDir.removeCallback(); + //} catch { + // // The "unsafeCleanup: true" option is supposed + // // to support a non-empty directory, but apparently + // // that isn't always the case. (see #8804) + // await fsextra.remove(tempDir.name); + //} + }); + return tempDir.name; + } + + private ensureSocketServer(): net.Server { + if (this.sockServer) { + return this.sockServer; + } + + const srv = net.createServer(); + this.sockServer = srv; + this.addCleanup(async () => { + try { + await new Promise((resolve) => srv.close(resolve)); + } catch (err) { + console.log(`failure while closing socket server: ${err}`); + } + }); + return srv; + } +} diff --git a/src/test/common/process/decoder.test.ts b/src/test/common/process/decoder.test.ts index 91a4dc21034a..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,24 +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 21eea8523d86..000000000000 --- a/src/test/common/process/execFactory.test.ts +++ /dev/null @@ -1,65 +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 { 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({})); - - }); - test('Ensure resource is used when getting configuration service settings (undefined resource)', async () => { - const pythonPath = `Python_Path_${new Date().toString()}`; - const pythonVersion = `Python_Version_${new Date().toString()}`; - const pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); - configService.setup(c => c.getSettings(TypeMoq.It.isValue(undefined))).returns(() => pythonSettings.object); - procService.setup(p => p.exec(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: pythonVersion })); - - const versionService = new InterpreterVersionService(procServiceFactory.object); - const version = await versionService.getVersion(pythonPath, ''); - - expect(version).to.be.equal(pythonVersion); - }); - test('Ensure resource is used when getting configuration service settings (defined resource)', async () => { - const resource = Uri.file('abc'); - const pythonPath = `Python_Path_${new Date().toString()}`; - const pythonVersion = `Python_Version_${new Date().toString()}`; - const pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); - configService.setup(c => c.getSettings(TypeMoq.It.isValue(resource))).returns(() => pythonSettings.object); - procService.setup(p => p.exec(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: pythonVersion })); - - const versionService = new InterpreterVersionService(procServiceFactory.object); - const version = await versionService.getVersion(pythonPath, ''); - - expect(version).to.be.equal(pythonVersion); - }); -}); diff --git a/src/test/common/process/logger.unit.test.ts b/src/test/common/process/logger.unit.test.ts new file mode 100644 index 000000000000..366a7056e89e --- /dev/null +++ b/src/test/common/process/logger.unit.test.ts @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; + +import { WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { ProcessLogger } from '../../../client/common/process/logger'; +import { getOSType, OSType } from '../../../client/common/utils/platform'; +import * as logging from '../../../client/logging'; +import { untildify } from '../../../client/common/helpers'; + +suite('ProcessLogger suite', () => { + 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(() => { + traceLogStub = sinon.stub(logging, 'traceLog'); + }); + + teardown(() => { + 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') }; + logger.logProcess('test', ['--foo', '--bar'], options); + + 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') }; + 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 preserves quotes around arguments if they contain spaces', async () => { + const options = { cwd: path.join('debug', 'path') }; + 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); + + 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') }; + logger.logProcess('test', ['--foo', '--bar'], options); + + 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 where the home path IS at the beginning of the path', async () => { + const options = { cwd: path.join('debug', 'path') }; + logger.logProcess(path.join(untildify('~'), 'test'), ['--foo', '--bar'], 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 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 () => { + logger.logProcess(path.join(untildify('~'), 'test'), ['--foo', '--bar']); + + 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 = {}; + logger.logProcess(path.join(untildify('~'), 'test'), ['--foo', '--bar'], options); + + 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 0edddb562da6..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 + if (isOs(OSType.Windows) && (await isPythonVersion('2.7'))) { 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,100 +61,165 @@ 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 pythonCode = ['import sys', 'import time', - 'print("1")', 'sys.stdout.flush()', 'time.sleep(1)', - 'print("2")', 'sys.stdout.flush()', 'time.sleep(1)', - 'print("3")']; + const procService = new ProcessService(); + const pythonCode = [ + 'import sys', + 'import time', + 'print("1")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'print("2")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'print("3")', + ]; const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')]); const outputs = ['1', '2', '3']; expect(result).not.to.be.an('undefined', 'result is undefined'); - const values = result.stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); + const values = result.stdout + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0); expect(values).to.deep.equal(outputs, 'Output values are incorrect'); expect(result.stderr).to.equal(undefined, 'stderr not undefined'); }); test('exec should wait for completion of program without new lines', async function () { - // tslint:disable-next-line:no-invalid-this this.timeout(5000); - const procService = new ProcessService(new BufferDecoder()); - const pythonCode = ['import sys', 'import time', - 'sys.stdout.write("1")', 'sys.stdout.flush()', 'time.sleep(1)', - 'sys.stdout.write("2")', 'sys.stdout.flush()', 'time.sleep(1)', - 'sys.stdout.write("3")']; + const procService = new ProcessService(); + const pythonCode = [ + 'import sys', + 'import time', + 'sys.stdout.write("1")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stdout.write("2")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stdout.write("3")', + ]; const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')]); const outputs = ['123']; expect(result).not.to.be.an('undefined', 'result is undefined'); - const values = result.stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); + const values = result.stdout + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0); expect(values).to.deep.equal(outputs, 'Output values are incorrect'); expect(result.stderr).to.equal(undefined, 'stderr not undefined'); }); test('exec should end when cancellationToken is cancelled', async function () { - // tslint:disable-next-line:no-invalid-this this.timeout(15000); - const procService = new ProcessService(new BufferDecoder()); - const pythonCode = ['import sys', 'import time', - 'print("1")', 'sys.stdout.flush()', 'time.sleep(10)', - 'print("2")', 'sys.stdout.flush()']; + const procService = new ProcessService(); + const pythonCode = [ + 'import sys', + 'import time', + 'print("1")', + 'sys.stdout.flush()', + 'time.sleep(10)', + 'print("2")', + 'sys.stdout.flush()', + ]; const cancellationToken = new CancellationTokenSource(); setTimeout(() => cancellationToken.cancel(), 3000); - const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')], { token: cancellationToken.token }); + const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')], { + token: cancellationToken.token, + }); expect(result).not.to.be.an('undefined', 'result is undefined'); - const values = result.stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); + const values = result.stdout + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0); expect(values).to.deep.equal(['1'], 'Output values are incorrect'); expect(result.stderr).to.equal(undefined, 'stderr not undefined'); }); - test('exec should stream stdout and stderr separately', async function () { - // tslint:disable-next-line:no-invalid-this + 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 pythonCode = ['import sys', 'import time', - 'print("1")', 'sys.stdout.flush()', 'time.sleep(1)', - 'sys.stderr.write("a")', 'sys.stderr.flush()', 'time.sleep(1)', - 'print("2")', 'sys.stdout.flush()', 'time.sleep(1)', - 'sys.stderr.write("b")', 'sys.stderr.flush()', 'time.sleep(1)', - 'print("3")', 'sys.stdout.flush()', 'time.sleep(1)', - 'sys.stderr.write("c")', 'sys.stderr.flush()']; + const procService = new ProcessService(); + const pythonCode = [ + 'print(">>>PYTHON-EXEC-OUTPUT")', + 'import sys', + 'import time', + 'print("1")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stderr.write("a")', + 'sys.stderr.flush()', + 'time.sleep(1)', + 'print("2")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stderr.write("b")', + 'sys.stderr.flush()', + 'time.sleep(1)', + 'print("3")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stderr.write("c")', + 'sys.stderr.flush()', + 'print("<<<PYTHON-EXEC-OUTPUT")', + ]; const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')]); const expectedStdout = ['1', '2', '3']; const expectedStderr = ['abc']; expect(result).not.to.be.an('undefined', 'result is undefined'); - const stdouts = result.stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); + const stdouts = result.stdout + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0); expect(stdouts).to.deep.equal(expectedStdout, 'stdout values are incorrect'); - const stderrs = result.stderr!.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); + const stderrs = result + .stderr!.split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0); expect(stderrs).to.deep.equal(expectedStderr, 'stderr values are incorrect'); }); test('exec should merge stdout and stderr streams', async function () { - // tslint:disable-next-line:no-invalid-this this.timeout(7000); - const procService = new ProcessService(new BufferDecoder()); - const pythonCode = ['import sys', 'import time', - 'sys.stdout.write("1")', 'sys.stdout.flush()', 'time.sleep(1)', - 'sys.stderr.write("a")', 'sys.stderr.flush()', 'time.sleep(1)', - 'sys.stdout.write("2")', 'sys.stdout.flush()', 'time.sleep(1)', - 'sys.stderr.write("b")', 'sys.stderr.flush()', 'time.sleep(1)', - 'sys.stdout.write("3")', 'sys.stdout.flush()', 'time.sleep(1)', - 'sys.stderr.write("c")', 'sys.stderr.flush()']; + const procService = new ProcessService(); + const pythonCode = [ + 'import sys', + 'import time', + 'sys.stdout.write("1")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stderr.write("a")', + 'sys.stderr.flush()', + 'time.sleep(1)', + 'sys.stdout.write("2")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stderr.write("b")', + 'sys.stderr.flush()', + 'time.sleep(1)', + 'sys.stdout.write("3")', + 'sys.stdout.flush()', + 'time.sleep(1)', + 'sys.stderr.write("c")', + 'sys.stderr.flush()', + ]; const result = await procService.exec(pythonPath, ['-c', pythonCode.join(';')], { mergeStdOutErr: true }); const expectedOutput = ['1a2b3c']; expect(result).not.to.be.an('undefined', 'result is undefined'); - const outputs = result.stdout.split(/\r?\n/g).map(line => line.trim()).filter(line => line.length > 0); + const outputs = result.stdout + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line.length > 0); expect(outputs).to.deep.equal(expectedOutput, 'Output values are incorrect'); }); test('exec should throw an error with stderr output', async () => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const pythonCode = ['import sys', 'sys.stderr.write("a")', 'sys.stderr.flush()']; const result = procService.exec(pythonPath, ['-c', pythonCode.join(';')], { throwOnStdErr: true }); @@ -154,31 +227,60 @@ 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(process.env); + let result = await procService.exec(pythonPath, ['-c', `import os;print(os.environ.get("MY_TEST_VARIABLE"))`], { + extraVariables: { MY_TEST_VARIABLE: 'foo' }, + }); + + expect(result).not.to.be.an('undefined', 'result is undefined'); + expect(result.stdout.trim()).to.be.equal('foo', 'Invalid output'); + expect(result.stderr).to.equal(undefined, 'stderr not undefined'); + + result = await procService.exec(pythonPath, ['-c', `import os;print(os.environ.get("MY_TEST_VARIABLE"))`]); + expect(result).not.to.be.an('undefined', 'result is undefined'); + expect(result.stdout.trim()).to.be.equal('None', 'Invalid output'); + expect(result.stderr).to.equal(undefined, 'stderr not undefined'); + }); }); diff --git a/src/test/common/process/proc.observable.test.ts b/src/test/common/process/proc.observable.test.ts index b9f301e5bb1d..debae38cc6eb 100644 --- a/src/test/common/process/proc.observable.test.ts +++ b/src/test/common/process/proc.observable.test.ts @@ -1,18 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { CancellationTokenSource } from 'vscode'; -import { BufferDecoder } from '../../../client/common/process/decoder'; + import { ProcessService } from '../../../client/common/process/proc'; import { createDeferred } from '../../../client/common/utils/async'; -import { getExtensionSettings } 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,225 +22,289 @@ 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 pythonCode = ['import sys', 'import time', - 'print("1")', 'sys.stdout.flush()', 'time.sleep(2)', - 'print("2")', 'sys.stdout.flush()', 'time.sleep(2)', - 'print("3")', 'sys.stdout.flush()', 'time.sleep(2)']; + const procService = new ProcessService(); + const pythonCode = [ + 'import sys', + 'import time', + 'print("1")', + 'sys.stdout.flush()', + 'time.sleep(2)', + 'print("2")', + 'sys.stdout.flush()', + 'time.sleep(2)', + 'print("3")', + 'sys.stdout.flush()', + 'time.sleep(2)', + ]; const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')]); const outputs = ['1', '2', '3']; expect(result).not.to.be.an('undefined', 'result is undefined'); - result.out.subscribe(output => { - // Ignore line breaks. - if (output.out.trim().length === 0) { - return; - } - const expectedValue = outputs.shift(); - if (expectedValue !== output.out.trim() && expectedValue === output.out) { - done(`Received value ${output.out} is not same as the expectd value ${expectedValue}`); - } - if (output.source !== 'stdout') { - done(`Source is not stdout. Value received is ${output.source}`); - } - }, done, done); + result.out.subscribe( + (output) => { + // Ignore line breaks. + if (output.out.trim().length === 0) { + return; + } + const expectedValue = outputs.shift(); + if (expectedValue !== output.out.trim() && expectedValue === output.out) { + done(`Received value ${output.out} is not same as the expectd value ${expectedValue}`); + } + if (output.source !== 'stdout') { + done(`Source is not stdout. Value received is ${output.source}`); + } + }, + done, + done, + ); }); test('execObservable should stream output without new lines', function (done) { - // tslint:disable-next-line:no-invalid-this + // Skipping to get nightly build to pass. Opened this issue: + // https://github.com/microsoft/vscode-python/issues/7411 + + this.skip(); + this.timeout(10000); - const procService = new ProcessService(new BufferDecoder()); - const pythonCode = ['import sys', 'import time', - 'sys.stdout.write("1")', 'sys.stdout.flush()', 'time.sleep(2)', - 'sys.stdout.write("2")', 'sys.stdout.flush()', 'time.sleep(2)', - 'sys.stdout.write("3")', 'sys.stdout.flush()', 'time.sleep(2)']; + const procService = new ProcessService(); + const pythonCode = [ + 'import sys', + 'import time', + 'sys.stdout.write("1")', + 'sys.stdout.flush()', + 'time.sleep(2)', + 'sys.stdout.write("2")', + 'sys.stdout.flush()', + 'time.sleep(2)', + 'sys.stdout.write("3")', + 'sys.stdout.flush()', + 'time.sleep(2)', + ]; const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')]); const outputs = ['1', '2', '3']; expect(result).not.to.be.an('undefined', 'result is undefined'); - result.out.subscribe(output => { - // Ignore line breaks. - if (output.out.trim().length === 0) { - return; - } - const expectedValue = outputs.shift(); - if (expectedValue !== output.out) { - done(`Received value ${output.out} is not same as the expectd value ${expectedValue}`); - } - if (output.source !== 'stdout') { - done(`Source is not stdout. Value received is ${output.source}`); - } - }, done, done); + result.out.subscribe( + (output) => { + // Ignore line breaks. + if (output.out.trim().length === 0) { + return; + } + const expectedValue = outputs.shift(); + if (expectedValue !== output.out) { + done(`Received value ${output.out} is not same as the expectd value ${expectedValue}`); + } + if (output.source !== 'stdout') { + done(`Source is not stdout. Value received is ${output.source}`); + } + }, + done, + done, + ); }); test('execObservable should end when cancellationToken is cancelled', function (done) { - // tslint:disable-next-line:no-invalid-this this.timeout(15000); - const procService = new ProcessService(new BufferDecoder()); - const pythonCode = ['import sys', 'import time', - 'print("1")', 'sys.stdout.flush()', 'time.sleep(10)', - 'print("2")', 'sys.stdout.flush()', 'time.sleep(2)']; + const procService = new ProcessService(); + const pythonCode = [ + 'import sys', + 'import time', + 'print("1")', + 'sys.stdout.flush()', + 'time.sleep(10)', + 'print("2")', + 'sys.stdout.flush()', + 'time.sleep(2)', + ]; const cancellationToken = new CancellationTokenSource(); - const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')], { token: cancellationToken.token }); + const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')], { + token: cancellationToken.token, + }); const def = createDeferred(); def.promise.then(done).catch(done); expect(result).not.to.be.an('undefined', 'result is undefined'); - result.out.subscribe(output => { - const value = output.out.trim(); - if (value === '1') { - cancellationToken.cancel(); - } else { - if (!def.completed) { - def.reject('Output received when we shouldn\'t have.'); + result.out.subscribe( + (output) => { + const value = output.out.trim(); + if (value === '1') { + cancellationToken.cancel(); + } else { + if (!def.completed) { + def.reject("Output received when we shouldn't have."); + } } - } - }, done, () => { - if (def.completed) { - return; - } - if (cancellationToken.token.isCancellationRequested) { - def.resolve(); - } else { - def.reject('Program terminated even before cancelling it.'); - } - }); + }, + done, + () => { + if (def.completed) { + return; + } + if (cancellationToken.token.isCancellationRequested) { + def.resolve(); + } else { + def.reject('Program terminated even before cancelling it.'); + } + }, + ); }); test('execObservable should end when process is killed', function (done) { - // tslint:disable-next-line:no-invalid-this this.timeout(15000); - const procService = new ProcessService(new BufferDecoder()); - const pythonCode = ['import sys', 'import time', - 'print("1")', 'sys.stdout.flush()', 'time.sleep(10)', - 'print("2")', 'sys.stdout.flush()', 'time.sleep(2)']; + const procService = new ProcessService(); + const pythonCode = [ + 'import sys', + 'import time', + 'print("1")', + 'sys.stdout.flush()', + 'time.sleep(10)', + 'print("2")', + 'sys.stdout.flush()', + 'time.sleep(2)', + ]; const cancellationToken = new CancellationTokenSource(); - const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')], { token: cancellationToken.token }); + const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')], { + token: cancellationToken.token, + }); let procKilled = false; expect(result).not.to.be.an('undefined', 'result is undefined'); - result.out.subscribe(output => { - const value = output.out.trim(); - // Ignore line breaks. - if (value.length === 0) { - return; - } - if (value === '1') { - procKilled = true; - if (result.proc) { - result.proc.kill(); + result.out.subscribe( + (output) => { + const value = output.out.trim(); + // Ignore line breaks. + if (value.length === 0) { + return; } - } else { - done('Output received when we shouldn\'t have.'); - } - }, done, () => { - const errorMsg = procKilled ? undefined : 'Program terminated even before killing it.'; - done(errorMsg); - }); + if (value === '1') { + procKilled = true; + if (result.proc) { + result.proc.kill(); + } + } else { + done("Output received when we shouldn't have."); + } + }, + done, + () => { + const errorMsg = procKilled ? undefined : 'Program terminated even before killing it.'; + done(errorMsg); + }, + ); }); - test('execObservable should stream stdout and stderr separately', function (done) { - // tslint:disable-next-line:no-invalid-this + 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 pythonCode = ['import sys', 'import time', - 'print("1")', 'sys.stdout.flush()', 'time.sleep(2)', - 'sys.stderr.write("a")', 'sys.stderr.flush()', 'time.sleep(2)', - 'print("2")', 'sys.stdout.flush()', 'time.sleep(2)', - 'sys.stderr.write("b")', 'sys.stderr.flush()', 'time.sleep(2)', - 'print("3")', 'sys.stdout.flush()', 'time.sleep(2)', - 'sys.stderr.write("c")', 'sys.stderr.flush()', 'time.sleep(2)']; + const procService = new ProcessService(); + const pythonCode = [ + 'print(">>>PYTHON-EXEC-OUTPUT")', + 'import sys', + 'import time', + 'print("1")', + 'sys.stdout.flush()', + 'time.sleep(2)', + 'sys.stderr.write("a")', + 'sys.stderr.flush()', + 'time.sleep(2)', + 'print("2")', + 'sys.stdout.flush()', + 'time.sleep(2)', + 'sys.stderr.write("b")', + 'sys.stderr.flush()', + 'time.sleep(2)', + 'print("3")', + 'sys.stdout.flush()', + 'time.sleep(2)', + 'sys.stderr.write("c")', + 'sys.stderr.flush()', + 'time.sleep(2)', + 'print("<<<PYTHON-EXEC-OUTPUT")', + ]; const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')]); const outputs = [ - { out: '1', source: 'stdout' }, { out: 'a', source: 'stderr' }, - { out: '2', source: 'stdout' }, { out: 'b', source: 'stderr' }, - { out: '3', source: 'stdout' }, { out: 'c', source: 'stderr' }]; + { out: '1', source: 'stdout' }, + { out: 'a', source: 'stderr' }, + { out: '2', source: 'stdout' }, + { out: 'b', source: 'stderr' }, + { out: '3', source: 'stdout' }, + { out: 'c', source: 'stderr' }, + ]; expect(result).not.to.be.an('undefined', 'result is undefined'); - result.out.subscribe(output => { - const value = output.out.trim(); - // Ignore line breaks. - if (value.length === 0) { - return; - } - const expectedOutput = outputs.shift()!; - - expect(value).to.be.equal(expectedOutput.out, 'Expected output is incorrect'); - expect(output.source).to.be.equal(expectedOutput.source, 'Expected sopurce is incorrect'); - }, done, done); - }); - - test('execObservable should send stdout and stderr streams separately', function (done) { - // tslint:disable-next-line:no-invalid-this - this.timeout(7000); - const procService = new ProcessService(new BufferDecoder()); - const pythonCode = ['import sys', 'import time', - 'print("1")', 'sys.stdout.flush()', 'time.sleep(1)', - 'sys.stderr.write("a")', 'sys.stderr.flush()', 'time.sleep(1)', - 'print("2")', 'sys.stdout.flush()', 'time.sleep(1)', - 'sys.stderr.write("b")', 'sys.stderr.flush()', 'time.sleep(1)', - 'print("3")', 'sys.stdout.flush()', 'time.sleep(1)', - 'sys.stderr.write("c")', 'sys.stderr.flush()', 'time.sleep(1)']; - const result = procService.execObservable(pythonPath, ['-c', pythonCode.join(';')], { mergeStdOutErr: true }); - const outputs = [ - { out: '1', source: 'stdout' }, { out: 'a', source: 'stderr' }, - { out: '2', source: 'stdout' }, { out: 'b', source: 'stderr' }, - { out: '3', source: 'stdout' }, { out: 'c', source: 'stderr' }]; + result.out.subscribe( + (output) => { + const value = output.out.trim(); + // Ignore line breaks. + if (value.length === 0) { + return; + } + const expectedOutput = outputs.shift()!; - expect(result).not.to.be.an('undefined', 'result is undefined'); - result.out.subscribe(output => { - const value = output.out.trim(); - // Ignore line breaks. - if (value.length === 0) { - return; - } - const expectedOutput = outputs.shift()!; - - expect(value).to.be.equal(expectedOutput.out, 'Expected output is incorrect'); - expect(output.source).to.be.equal(expectedOutput.source, 'Expected sopurce is incorrect'); - }, done, done); + expect(value).to.be.equal(expectedOutput.out, 'Expected output is incorrect'); + expect(output.source).to.be.equal(expectedOutput.source, 'Expected sopurce is incorrect'); + }, + done, + done, + ); + }); + test('execObservable should send stdout and stderr streams separately', async function () { + // This test is failing on Windows. Tracked by GH #4755. + if (isOs(OSType.Windows)) { + 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 }); expect(result).not.to.be.an('undefined', 'result is undefined.'); - result.out.subscribe(output => { - done('Output received, when we\'re expecting an error to be thrown.'); - }, (ex: Error) => { - expect(ex).to.have.property('message', 'a', 'Invalid error thrown'); - done(); - }, () => { - done('Completed, when we\'re expecting an error to be thrown.'); - }); + result.out.subscribe( + (_output) => { + done("Output received, when we're expecting an error to be thrown."); + }, + (ex: Error) => { + expect(ex).to.have.property('message', 'a', 'Invalid error thrown'); + done(); + }, + () => { + done("Completed, when we're expecting an error to be thrown."); + }, + ); }); test('execObservable should throw an error when spawn file not found', (done) => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const result = procService.execObservable(Date.now().toString(), []); expect(result).not.to.be.an('undefined', 'result is undefined.'); - result.out.subscribe(output => { - done('Output received, when we\'re expecting an error to be thrown.'); - }, ex => { - expect(ex).to.have.property('code', 'ENOENT', 'Invalid error code'); - done(); - }, () => { - done('Completed, when we\'re expecting an error to be thrown.'); - }); + result.out.subscribe( + (_output) => { + done("Output received, when we're expecting an error to be thrown."); + }, + (ex) => { + expect(ex).to.have.property('code', 'ENOENT', 'Invalid error code'); + done(); + }, + () => { + done("Completed, when we're expecting an error to be thrown."); + }, + ); }); test('execObservable should exit without no output', (done) => { - const procService = new ProcessService(new BufferDecoder()); + const procService = new ProcessService(); const result = procService.execObservable(pythonPath, ['-c', 'import sys', 'sys.exit()']); expect(result).not.to.be.an('undefined', 'result is undefined.'); - result.out.subscribe(output => { - done(`Output received, when we\'re not expecting any, ${JSON.stringify(output)}`); - }, done, done); + result.out.subscribe( + (output) => { + done(`Output received, when we\'re not expecting any, ${JSON.stringify(output)}`); + }, + done, + done, + ); }); }); diff --git a/src/test/common/process/proc.unit.test.ts b/src/test/common/process/proc.unit.test.ts index 4ba9c0c33dde..38cf450bef57 100644 --- a/src/test/common/process/proc.unit.test.ts +++ b/src/test/common/process/proc.unit.test.ts @@ -3,51 +3,51 @@ 'use strict'; -// tslint:disable:no-any max-func-body-length no-invalid-this max-classes-per-file - import { expect } from 'chai'; -import { spawn } from 'child_process'; +import { ChildProcess, spawn } from 'child_process'; import { ProcessService } from '../../../client/common/process/proc'; -import { createDeferred } from '../../../client/common/utils/async'; +import { createDeferred, Deferred } from '../../../client/common/utils/async'; import { PYTHON_PATH } from '../../common'; +interface IProcData { + proc: ChildProcess; + exited: Deferred<Boolean>; +} + suite('Process - Process Service', function () { - // tslint:disable-next-line:no-invalid-this this.timeout(5000); - let procIdsToKill: number[] = []; + const procsToKill: IProcData[] = []; teardown(() => { - // tslint:disable-next-line:no-require-imports - const killProcessTree = require('tree-kill'); - procIdsToKill.forEach(pid => { - try { - killProcessTree(pid); - } catch { - // Ignore. + procsToKill.forEach((p) => { + if (!p.exited.resolved) { + p.proc.kill(); } }); - procIdsToKill = []; }); - function spawnProc() { + function spawnProc(): IProcData { const proc = spawn(PYTHON_PATH, ['-c', 'while(True): import time;time.sleep(0.5);print(1)']); const exited = createDeferred<Boolean>(); proc.on('exit', () => exited.resolve(true)); - procIdsToKill.push(proc.pid); + procsToKill.push({ proc, exited }); - return { pid: proc.pid, exited: exited.promise }; + return procsToKill[procsToKill.length - 1]; } test('Process is killed', async () => { const proc = spawnProc(); + expect(proc.proc.pid !== undefined).to.equal(true, 'invalid pid'); + if (proc.proc.pid) { + ProcessService.kill(proc.proc.pid); + } - ProcessService.kill(proc.pid); - - expect(await proc.exited).to.equal(true, 'process did not die'); + expect(await proc.exited.promise).to.equal(true, 'process did not die'); }); test('Process is alive', async () => { const proc = spawnProc(); - - expect(ProcessService.isAlive(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 new file mode 100644 index 000000000000..5adcdeccecfd --- /dev/null +++ b/src/test/common/process/processFactory.unit.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; +import { expect } from 'chai'; +import { instance, mock, verify, when } from 'ts-mockito'; +import { Disposable, Uri } from 'vscode'; + +import { ProcessLogger } from '../../../client/common/process/logger'; +import { ProcessService } from '../../../client/common/process/proc'; +import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; +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'; + +suite('Process - ProcessServiceFactory', () => { + let factory: ProcessServiceFactory; + let envVariablesProvider: IEnvironmentVariablesProvider; + let processLogger: IProcessLogger; + let processService: ProcessService; + let disposableRegistry: IDisposableRegistry; + + setup(() => { + envVariablesProvider = mock(EnvironmentVariablesProvider); + processLogger = mock(ProcessLogger); + when(processLogger.logProcess('', [], {})).thenReturn(); + processService = mock(ProcessService); + when( + processService.on('exec', () => { + return; + }), + ).thenReturn(processService); + disposableRegistry = []; + factory = new ProcessServiceFactory( + instance(envVariablesProvider), + instance(processLogger), + disposableRegistry, + ); + }); + + teardown(() => { + (disposableRegistry as Disposable[]).forEach((d) => d.dispose()); + }); + + [Uri.parse('test'), undefined].forEach((resource) => { + test(`Ensure ProcessService is created with an ${resource ? 'existing' : 'undefined'} resource`, async () => { + when(envVariablesProvider.getEnvironmentVariables(resource)).thenResolve({ x: 'test' }); + + const proc = await factory.create(resource); + verify(envVariablesProvider.getEnvironmentVariables(resource)).once(); + + const disposables = disposableRegistry as Disposable[]; + expect(disposables.length).equal(1); + expect(proc).instanceOf(ProcessService); + }); + }); +}); diff --git a/src/test/common/process/pythonEnvironment.unit.test.ts b/src/test/common/process/pythonEnvironment.unit.test.ts new file mode 100644 index 000000000000..a2cca66d08be --- /dev/null +++ b/src/test/common/process/pythonEnvironment.unit.test.ts @@ -0,0 +1,362 @@ +// 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 sinon from 'sinon'; +import { SemVer } from 'semver'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { + createCondaEnv, + createPythonEnv, + createMicrosoftStoreEnv, +} from '../../../client/common/process/pythonEnvironment'; +import { IProcessService, StdErrError } from '../../../client/common/process/types'; +import { Architecture } from '../../../client/common/utils/platform'; +import { Conda } from '../../../client/pythonEnvironments/common/environmentManagers/conda'; +import { OUTPUT_MARKER_SCRIPT } from '../../../client/common/process/internal/scripts'; + +use(chaiAsPromised.default); + +suite('PythonEnvironment', () => { + let processService: TypeMoq.IMock<IProcessService>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + const pythonPath = 'path/to/python'; + + setup(() => { + processService = TypeMoq.Mock.ofType<IProcessService>(undefined, TypeMoq.MockBehavior.Strict); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(undefined, TypeMoq.MockBehavior.Strict); + }); + + test('getInterpreterInformation should return an object if the python path is valid', async () => { + const json = { + versionInfo: [3, 7, 5, 'candidate', 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, + }; + + 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-candidate1'), + sysPrefix: json.sysPrefix, + sysVersion: undefined, + }; + + expect(result).to.deep.equal(expectedResult, 'Incorrect value returned by getInterpreterInformation().'); + }); + + test('getInterpreterInformation should return an object if the version info contains less than 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, + }; + + processService + .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + stdout: JSON.stringify(json), + }), + ); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.getInterpreterInformation(); + const expectedResult = { + architecture: Architecture.x64, + path: pythonPath, + version: new SemVer('3.7.5'), + sysPrefix: json.sysPrefix, + sysVersion: undefined, + }; + + expect(result).to.deep.equal( + expectedResult, + 'Incorrect value returned by getInterpreterInformation() with truncated versionInfo.', + ); + }); + + test('getInterpreterInformation should return an object with the architecture value set to x86 if json.is64bit is not 64bit', async () => { + const json = { + versionInfo: [3, 7, 5, 'candidate'], + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: false, + }; + + processService + .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + stdout: JSON.stringify(json), + }), + ); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.getInterpreterInformation(); + const expectedResult = { + architecture: Architecture.x86, + path: pythonPath, + version: new SemVer('3.7.5-candidate'), + sysPrefix: json.sysPrefix, + sysVersion: undefined, + }; + + expect(result).to.deep.equal( + expectedResult, + 'Incorrect value returned by getInterpreterInformation() for x86b architecture.', + ); + }); + + test('getInterpreterInformation should error out if interpreterInfo.py times out', async () => { + processService + .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + + .returns(() => Promise.reject(new Error('timed out'))); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.getInterpreterInformation(); + + expect(result).to.equal( + undefined, + 'getInterpreterInfo() should return undefined because interpreterInfo timed out.', + ); + }); + + test('getInterpreterInformation should return undefined if the json value returned by interpreterInfo.py is not valid', async () => { + processService + .setup((p) => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ stdout: 'bad json' })); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.getInterpreterInformation(); + + expect(result).to.equal(undefined, 'getInterpreterInfo() should return undefined because of bad json.'); + }); + + test('getExecutablePath should return pythonPath if pythonPath is a file', async () => { + fileSystem.setup((f) => f.pathExists(pythonPath)).returns(() => Promise.resolve(true)); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.getExecutablePath(); + + expect(result).to.equal(pythonPath, "getExecutablePath() sbould return pythonPath if it's a file"); + }); + + test('getExecutablePath should not return pythonPath if pythonPath is not a file', async () => { + const executablePath = 'path/to/dummy/executable'; + fileSystem.setup((f) => f.pathExists(pythonPath)).returns(() => Promise.resolve(false)); + processService + .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); + + const result = await env.getExecutablePath(); + + expect(result).to.equal(executablePath, "getExecutablePath() sbould not return pythonPath if it's not a file"); + }); + + test('getExecutablePath should return `undefined` if the result of exec() writes to stderr', async () => { + const stderr = 'bar'; + fileSystem.setup((f) => f.pathExists(pythonPath)).returns(() => Promise.resolve(false)); + processService + .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 = await env.getExecutablePath(); + + expect(result).to.be.equal(undefined); + }); + + test('isModuleInstalled should call processService.exec()', async () => { + const moduleName = 'foo'; + const argv = ['-c', `import ${moduleName}`]; + processService + .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .returns(() => Promise.resolve({ stdout: '' })) + .verifiable(TypeMoq.Times.once()); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + await env.isModuleInstalled(moduleName); + + processService.verifyAll(); + }); + + test('isModuleInstalled should return true when processService.exec() succeeds', async () => { + const moduleName = 'foo'; + const argv = ['-c', `import ${moduleName}`]; + processService + .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .returns(() => Promise.resolve({ stdout: '' })); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.isModuleInstalled(moduleName); + + expect(result).to.equal(true, 'isModuleInstalled() should return true if the module exists'); + }); + + test('isModuleInstalled should return false when processService.exec() throws', async () => { + const moduleName = 'foo'; + const argv = ['-c', `import ${moduleName}`]; + processService + .setup((p) => p.exec(pythonPath, argv, { throwOnStdErr: true })) + .returns(() => Promise.reject(new StdErrError('bar'))); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = await env.isModuleInstalled(moduleName); + + expect(result).to.equal(false, 'isModuleInstalled() should return false if the module does not exist'); + }); + + test('getExecutionInfo should return pythonPath and the execution arguments as is', () => { + const args = ['-a', 'b', '-c']; + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + + const result = env.getExecutionInfo(args); + + expect(result).to.deep.equal( + { command: pythonPath, args, python: [pythonPath], pythonExecutable: pythonPath }, + 'getExecutionInfo should return pythonPath and the command and execution arguments as is', + ); + }); +}); + +suite('CondaEnvironment', () => { + let processService: TypeMoq.IMock<IProcessService>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + const args = ['-a', 'b', '-c']; + const pythonPath = 'path/to/python'; + const condaFile = 'path/to/conda'; + + setup(() => { + 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); + }); + + 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 = await createCondaEnv(condaInfo, processService.object, fileSystem.object); + + const result = env?.getExecutionInfo(args, pythonPath); + + expect(result).to.deep.equal({ + 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, + }); + }); + + test('getExecutionInfo with a non-named environment should return execution info using the environment path', async () => { + const condaInfo = { name: '', path: 'bar' }; + const env = await createCondaEnv(condaInfo, processService.object, fileSystem.object); + + const result = env?.getExecutionInfo(args, pythonPath); + + expect(result).to.deep.equal({ + 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, + }); + }); + + 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 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, pythonPath); + + expect(result).to.deep.equal(expected); + }); + + test('getExecutionObservableInfo with a non-named environment should return execution info using conda full path', async () => { + const condaInfo = { name: '', path: 'bar' }; + 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, pythonPath); + + expect(result).to.deep.equal(expected); + }); +}); + +suite('MicrosoftStoreEnvironment', () => { + let processService: TypeMoq.IMock<IProcessService>; + const pythonPath = 'foo'; + + setup(() => { + processService = TypeMoq.Mock.ofType<IProcessService>(undefined, TypeMoq.MockBehavior.Strict); + }); + + test('Should return pythonPath if it is the path to the microsoft store interpreter', async () => { + const env = createMicrosoftStoreEnv(pythonPath, processService.object); + + const executablePath = await env.getExecutablePath(); + + expect(executablePath).to.equal(pythonPath); + processService.verifyAll(); + }); +}); diff --git a/src/test/common/process/pythonExecutionFactory.unit.test.ts b/src/test/common/process/pythonExecutionFactory.unit.test.ts new file mode 100644 index 000000000000..0981c59e78bb --- /dev/null +++ b/src/test/common/process/pythonExecutionFactory.unit.test.ts @@ -0,0 +1,423 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect } from 'chai'; +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'; +import { Uri } from 'vscode'; + +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { ProcessLogger } from '../../../client/common/process/logger'; +import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; +import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; +import { + IProcessLogger, + IProcessService, + IProcessServiceFactory, + IPythonExecutionService, +} from '../../../client/common/process/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 { + IActivatedEnvironmentLaunch, + IComponentAdapter, + IInterpreterService, +} from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { ServiceContainer } from '../../../client/ioc/container'; +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: PythonEnvironment = { + path: '/foo/bar/python.exe', + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, +}; + +function title(resource?: Uri, interpreter?: PythonEnvironment) { + return `${resource ? 'With a resource' : 'Without a resource'}${interpreter ? ' and an interpreter' : ''}`; +} + +async function verifyCreateActivated( + factory: PythonExecutionFactory, + activationHelper: IEnvironmentActivationService, + resource?: Uri, + interpreter?: PythonEnvironment, +): Promise<IPythonExecutionService> { + when(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).thenResolve(); + + const service = await factory.createActivatedEnvironment({ resource, interpreter }); + + verify(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).once(); + + return service; +} + +suite('Process - PythonExecutionFactory', () => { + [ + { resource: undefined, interpreter: undefined }, + { resource: undefined, interpreter: pythonInterpreter }, + { resource: Uri.parse('x'), interpreter: undefined }, + { resource: Uri.parse('x'), interpreter: pythonInterpreter }, + ].forEach((item) => { + const { resource } = item; + const { interpreter } = item; + suite(title(resource, interpreter), () => { + let factory: PythonExecutionFactory; + let activationHelper: IEnvironmentActivationService; + let activatedEnvironmentLaunch: IActivatedEnvironmentLaunch; + let processFactory: IProcessServiceFactory; + let configService: IConfigurationService; + let processLogger: IProcessLogger; + let processService: typemoq.IMock<IProcessService>; + let interpreterService: IInterpreterService; + let pyenvs: IComponentAdapter; + let executionService: typemoq.IMock<IPythonExecutionService>; + let autoSelection: IInterpreterAutoSelectionService; + let interpreterPathExpHelper: IInterpreterPathService; + let getPixiEnvironmentFromInterpreterStub: sinon.SinonStub; + let getPixiStub: sinon.SinonStub; + const pythonPath = 'path/to/python'; + setup(() => { + 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); + processLogger = mock(ProcessLogger); + 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', () => { + /** 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 }, + // 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), + ); + 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), + ); + when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn( + instance(configService), + ); + factory = new PythonExecutionFactory( + instance(serviceContainer), + instance(activationHelper), + instance(processFactory), + instance(configService), + 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); + when(activationHelper.getActivatedEnvironmentVariables(resource)).thenResolve({ x: '1' }); + when(pythonSettings.pythonPath).thenReturn('HELLO'); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + + const service = await factory.create({ resource }); + + expect(service).to.not.equal(undefined); + verify(processFactory.create(resource)).once(); + verify(pythonSettings.pythonPath).once(); + }); + + test('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 pythonSettings = mock(PythonSettings); + + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + + let createInvoked = false; + const mockExecService = 'something'; + factory.create = async () => { + 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.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 pythonSettings = mock(PythonSettings); + + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + + let createInvoked = false; + const mockExecService = 'something'; + factory.create = async () => { + 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.strictEqual(createInvoked, true); + }); + test('PythonExecutionService is created', async () => { + let createInvoked = false; + const mockExecService = 'something'; + 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', + }); + when(pythonSettings.pythonPath).thenReturn('HELLO'); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + const service = await factory.createActivatedEnvironment({ resource, interpreter }); + + expect(service).to.not.equal(undefined); + verify(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).once(); + if (!interpreter) { + verify(pythonSettings.pythonPath).once(); + } + assert.strictEqual(createInvoked, false); + }); + + 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)); + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(new SemVer(CONDA_RUN_VERSION)); + when(pyenvs.getCondaEnvironment(pythonPath)).thenResolve({ + name: 'foo', + path: 'path/to/foo/env', + }); + + const service = await factory.create({ resource }); + + expect(service).to.not.equal(undefined); + verify(processFactory.create(resource)).once(); + verify(pythonSettings.pythonPath).once(); + verify(pyenvs.getCondaEnvironment(pythonPath)).once(); + }); + + 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)); + 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(pyenvs.getCondaEnvironment(pythonPath)).once(); + }); + + 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', + }); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(new SemVer(CONDA_RUN_VERSION)); + when(pyenvs.getCondaEnvironment(anyString())).thenResolve({ + name: 'foo', + path: 'path/to/foo/env', + }); + + const service = await factory.createActivatedEnvironment({ resource, interpreter }); + + expect(service).to.not.equal(undefined); + if (!interpreter) { + verify(pythonSettings.pythonPath).once(); + verify(pyenvs.getCondaEnvironment(pythonPath)).once(); + } else { + 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 () => { + let createInvoked = false; + + const mockExecService = 'mockService'; + 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', + }); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + 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(activationHelper.getActivatedEnvironmentVariables(resource, anything(), anything())).once(); + if (!interpreter) { + verify(pythonSettings.pythonPath).once(); + } + + assert.strictEqual(createInvoked, false); + }); + + test('Ensure `createCondaExecutionService` creates a CondaExecutionService instance if there is a conda environment', async () => { + when(pyenvs.getCondaEnvironment(pythonPath)).thenResolve({ + name: 'foo', + path: 'path/to/foo/env', + }); + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(new SemVer(CONDA_RUN_VERSION)); + + const result = await factory.createCondaExecutionService(pythonPath, processService.object); + + expect(result).to.not.equal(undefined); + verify(pyenvs.getCondaEnvironment(pythonPath)).once(); + }); + + test('Ensure `createCondaExecutionService` returns undefined if there is no conda environment', async () => { + 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', + ); + verify(pyenvs.getCondaEnvironment(pythonPath)).once(); + }); + + test('Ensure `createCondaExecutionService` returns undefined if the conda version does not support conda run', async () => { + 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', + ); + 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 0ae91f6b7632..fc4fbf5328a9 100644 --- a/src/test/common/process/pythonProc.simple.multiroot.test.ts +++ b/src/test/common/process/pythonProc.simple.multiroot.test.ts @@ -6,147 +6,107 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { execFile } from 'child_process'; -import * as fs from 'fs-extra'; -import { Container } from 'inversify'; -import { EOL } from 'os'; import * as path from 'path'; -import { ConfigurationTarget, Disposable, 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 { IS_WINDOWS } from '../../../client/common/platform/constants'; -import { FileSystem } from '../../../client/common/platform/fileSystem'; -import { PathUtils } from '../../../client/common/platform/pathUtils'; -import { PlatformService } from '../../../client/common/platform/platformService'; -import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; -import { CurrentProcess } from '../../../client/common/process/currentProcess'; -import { registerTypes as processRegisterTypes } from '../../../client/common/process/serviceRegistry'; +import { ConfigurationTarget, Uri } from 'vscode'; +import * as fs from '../../../client/common/platform/fs-paths'; import { IPythonExecutionFactory, StdErrError } from '../../../client/common/process/types'; -import { - IConfigurationService, ICurrentProcess, - IDisposableRegistry, IPathUtils, IsWindows -} from '../../../client/common/types'; -import { OSType } from '../../../client/common/utils/platform'; -import { - registerTypes as variablesRegisterTypes -} from '../../../client/common/variables/serviceRegistry'; -import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../../client/interpreter/autoSelection/types'; -import { ServiceContainer } from '../../../client/ioc/container'; -import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IConfigurationService } from '../../../client/common/types'; +import { clearCache } from '../../../client/common/utils/cacheUtils'; import { IServiceContainer } from '../../../client/ioc/types'; -import { - clearPythonPathInWorkspaceFolder, getExtensionSettings, - isOs, - isPythonVersion -} from '../../common'; -import { MockAutoSelectionService } from '../../mocks/autoSelector'; -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 cont: Container; let serviceContainer: IServiceContainer; let configService: IConfigurationService; let pythonExecFactory: IPythonExecutionFactory; suiteSetup(async function () { if (!IS_MULTI_ROOT_TEST) { - // tslint:disable-next-line:no-invalid-this this.skip(); } await clearPythonPathInWorkspaceFolder(workspace4Path); - await initialize(); + serviceContainer = (await initialize()).serviceContainer; }); setup(async () => { - cont = new Container(); - serviceContainer = new ServiceContainer(cont); - const serviceManager = new ServiceManager(cont); - - serviceManager.addSingletonInstance<IServiceContainer>(IServiceContainer, serviceContainer); - serviceManager.addSingletonInstance<Disposable[]>(IDisposableRegistry, []); - serviceManager.addSingletonInstance<boolean>(IsWindows, IS_WINDOWS); - serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils); - serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess); - serviceManager.addSingleton<IConfigurationService>(IConfigurationService, ConfigurationService); - serviceManager.addSingleton<IPlatformService>(IPlatformService, PlatformService); - serviceManager.addSingleton<IWorkspaceService>(IWorkspaceService, WorkspaceService); - serviceManager.addSingleton<IFileSystem>(IFileSystem, FileSystem); - serviceManager.addSingleton<IInterpreterAutoSelectionService>(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); - processRegisterTypes(serviceManager); - variablesRegisterTypes(serviceManager); - - configService = serviceManager.get<IConfigurationService>(IConfigurationService); + initializeExternalDependencies(serviceContainer); + configService = serviceContainer.get<IConfigurationService>(IConfigurationService); pythonExecFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); await configService.updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + clearCache(); return initializeTest(); }); suiteTeardown(closeActiveWindows); teardown(async () => { - cont.unbindAll(); - cont.unload(); await closeActiveWindows(); await clearPythonPathInWorkspaceFolder(workspace4Path); await configService.updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); await initializeTest(); + clearCache(); }); test('Importing without a valid PYTHONPATH should fail', async () => { - await configService.updateSetting('envFile', 'someInvalidFile.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); + await configService.updateSetting( + 'envFile', + 'someInvalidFile.env', + workspace4PyFile, + ConfigurationTarget.WorkspaceFolder, + ); pythonExecFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); - const promise = pythonExecService.exec([workspace4PyFile.fsPath], { cwd: path.dirname(workspace4PyFile.fsPath), throwOnStdErr: true }); + const promise = pythonExecService.exec([workspace4PyFile.fsPath], { + cwd: path.dirname(workspace4PyFile.fsPath), + throwOnStdErr: true, + }); await expect(promise).to.eventually.be.rejectedWith(StdErrError); - }); - - test('Importing with a valid PYTHONPATH from .env file should succeed', async function () { - // This test has not been working for many months in Python 2.7 under - // Windows. Tracked by #2547. - if (isOs(OSType.Windows) && await isPythonVersion('2.7')) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } + }).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], { cwd: path.dirname(workspace4PyFile.fsPath), throwOnStdErr: true }); + const result = await pythonExecService.exec([workspace4PyFile.fsPath], { + cwd: path.dirname(workspace4PyFile.fsPath), + 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 () => { + test("Known modules such as 'os' and 'sys' should be deemed 'installed'", async () => { const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); const osModuleIsInstalled = pythonExecService.isModuleInstalled('os'); const sysModuleIsInstalled = pythonExecService.isModuleInstalled('sys'); await expect(osModuleIsInstalled).to.eventually.equal(true, 'os module is not installed'); await expect(sysModuleIsInstalled).to.eventually.equal(true, 'sys module is not installed'); - }); + }).timeout(TEST_TIMEOUT * 3); - test('Unknown modules such as \'xyzabc123\' be deemed \'not installed\'', async () => { + test("Unknown modules such as 'xyzabc123' be deemed 'not installed'", async () => { const pythonExecService = await pythonExecFactory.create({ resource: workspace4PyFile }); const randomModuleName = `xyz123${new Date().getSeconds()}`; const randomModuleIsInstalled = pythonExecService.isModuleInstalled(randomModuleName); - await expect(randomModuleIsInstalled).to.eventually.equal(false, `Random module '${randomModuleName}' is installed`); - }); + await expect(randomModuleIsInstalled).to.eventually.equal( + false, + `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; } else { - expectedExecutablePath = await new Promise<string>(resolve => { + expectedExecutablePath = await new Promise<string>((resolve) => { execFile(pythonPath, ['-c', 'import sys;print(sys.executable)'], (_error, stdout, _stdErr) => { resolve(stdout.trim()); }); @@ -155,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 new file mode 100644 index 000000000000..7382fc9f9869 --- /dev/null +++ b/src/test/common/process/pythonProcess.unit.test.ts @@ -0,0 +1,119 @@ +// 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 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 { noop } from '../../core'; + +use(chaiAsPromised.default); + +suite('PythonProcessService', () => { + let processService: TypeMoq.IMock<IProcessService>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + const pythonPath = 'path/to/python'; + + setup(() => { + processService = TypeMoq.Mock.ofType<IProcessService>(undefined, TypeMoq.MockBehavior.Strict); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(undefined, TypeMoq.MockBehavior.Strict); + }); + + test('execObservable should call processService.execObservable', () => { + const args = ['-a', 'b', '-c']; + const options = {}; + const observable = { + proc: undefined, + + out: {} as any, + dispose: () => { + noop(); + }, + }; + processService.setup((p) => p.execObservable(pythonPath, args, options)).returns(() => observable); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + const procs = createPythonProcessService(processService.object, env); + + const result = procs.execObservable(args, options); + + processService.verify((p) => p.execObservable(pythonPath, args, options), TypeMoq.Times.once()); + expect(result).to.be.equal(observable, 'execObservable should return an observable'); + }); + + test('execModuleObservable should call processService.execObservable with the -m argument', () => { + const args = ['-a', 'b', '-c']; + const moduleName = 'foo'; + const expectedArgs = ['-m', moduleName, ...args]; + const options = {}; + const observable = { + proc: undefined, + + out: {} as any, + dispose: () => { + noop(); + }, + }; + processService.setup((p) => p.execObservable(pythonPath, expectedArgs, options)).returns(() => observable); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + const procs = createPythonProcessService(processService.object, env); + + const result = procs.execModuleObservable(moduleName, args, options); + + processService.verify((p) => p.execObservable(pythonPath, expectedArgs, options), TypeMoq.Times.once()); + expect(result).to.be.equal(observable, 'execModuleObservable should return an observable'); + }); + + test('exec should call processService.exec', async () => { + const args = ['-a', 'b', '-c']; + const options = {}; + const stdout = 'foo'; + processService.setup((p) => p.exec(pythonPath, args, options)).returns(() => Promise.resolve({ stdout })); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + const procs = createPythonProcessService(processService.object, env); + + const result = await procs.exec(args, options); + + processService.verify((p) => p.exec(pythonPath, args, options), TypeMoq.Times.once()); + expect(result.stdout).to.be.equal(stdout, 'exec should return the content of stdout'); + }); + + test('execModule should call processService.exec with the -m argument', async () => { + const args = ['-a', 'b', '-c']; + const moduleName = 'foo'; + const expectedArgs = ['-m', moduleName, ...args]; + const options = {}; + const stdout = 'bar'; + processService + .setup((p) => p.exec(pythonPath, expectedArgs, options)) + .returns(() => Promise.resolve({ stdout })); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + const procs = createPythonProcessService(processService.object, env); + + const result = await procs.execModule(moduleName, args, options); + + processService.verify((p) => p.exec(pythonPath, expectedArgs, options), TypeMoq.Times.once()); + expect(result.stdout).to.be.equal(stdout, 'exec should return the content of stdout'); + }); + + test('execModule should throw an error if the module is not installed', async () => { + const args = ['-a', 'b', '-c']; + const moduleName = 'foo'; + const expectedArgs = ['-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, ['-c', `import ${moduleName}`], { throwOnStdErr: true })) + .returns(() => Promise.reject(new StdErrError('not installed'))); + const env = createPythonEnv(pythonPath, processService.object, fileSystem.object); + const procs = createPythonProcessService(processService.object, env); + + const result = procs.execModule(moduleName, args, options); + + expect(result).to.eventually.be.rejectedWith(`Module '${moduleName}' not installed`); + }); +}); diff --git a/src/test/common/process/pythonToolService.unit.test.ts b/src/test/common/process/pythonToolService.unit.test.ts new file mode 100644 index 000000000000..bef199ce223a --- /dev/null +++ b/src/test/common/process/pythonToolService.unit.test.ts @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { ProcessService } from '../../../client/common/process/proc'; +import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; +import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; +import { PythonToolExecutionService } from '../../../client/common/process/pythonToolService'; +import { + ExecutionResult, + IProcessService, + IProcessServiceFactory, + IPythonExecutionFactory, + IPythonExecutionService, + ObservableExecutionResult, +} from '../../../client/common/process/types'; +import { ExecutionInfo } from '../../../client/common/types'; +import { ServiceContainer } from '../../../client/ioc/container'; +import { noop } from '../../core'; + +use(chaiAsPromised.default); + +suite('Process - Python tool execution service', () => { + const resource = Uri.parse('one'); + const observable: ObservableExecutionResult<string> = { + proc: undefined, + + out: {} as any, + dispose: () => { + noop(); + }, + }; + const executionResult: ExecutionResult<string> = { + stdout: 'output', + }; + + let pythonService: IPythonExecutionService; + let executionFactory: IPythonExecutionFactory; + let processService: IProcessService; + let processFactory: IProcessServiceFactory; + + let executionService: PythonToolExecutionService; + + setup(() => { + pythonService = mock<IPythonExecutionService>(); + when(pythonService.execModuleObservable(anything(), anything(), anything())).thenReturn(observable); + when(pythonService.execModule(anything(), anything(), anything())).thenResolve(executionResult); + const pythonServiceInstance = instance(pythonService); + + (pythonServiceInstance as any).then = undefined; + + executionFactory = mock(PythonExecutionFactory); + when(executionFactory.create(anything())).thenResolve(pythonServiceInstance); + + processService = mock(ProcessService); + when(processService.execObservable(anything(), anything(), anything())).thenReturn(observable); + when(processService.exec(anything(), anything(), anything())).thenResolve(executionResult); + + processFactory = mock(ProcessServiceFactory); + when(processFactory.create(anything())).thenResolve(instance(processService)); + + const serviceContainer = mock(ServiceContainer); + when(serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory)).thenReturn( + instance(executionFactory), + ); + when(serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory)).thenReturn(instance(processFactory)); + + executionService = new PythonToolExecutionService(instance(serviceContainer)); + }); + + test('When calling execObservable, throw an error if environment variables are passed to the options parameter', () => { + const options = { env: { envOne: 'envOne' } }; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + moduleName: 'moduleOne', + args: ['-a', 'b', '-c'], + }; + + const promise = executionService.execObservable(executionInfo, options, resource); + + expect(promise).to.eventually.be.rejectedWith('Environment variables are not supported'); + }); + + test('When calling execObservable, use a python execution service if a module name is passed to the execution info', async () => { + const options = {}; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + moduleName: 'moduleOne', + args: ['-a', 'b', '-c'], + }; + + const result = await executionService.execObservable(executionInfo, options, resource); + + assert.deepEqual(result, observable); + verify(pythonService.execModuleObservable(executionInfo.moduleName!, executionInfo.args, options)).once(); + }); + + test('When calling execObservable, use a process service if an empty module name string is passed to the execution info', async () => { + const options = {}; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + moduleName: '', + args: ['-a', 'b', '-c'], + }; + + const result = await executionService.execObservable(executionInfo, options, resource); + + assert.deepEqual(result, observable); + verify(processService.execObservable(executionInfo.execPath!, executionInfo.args, anything())).once(); + }); + + test('When calling execObservable, use a process service if no module name is passed to the execution info', async () => { + const options = {}; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + args: ['-a', 'b', '-c'], + }; + + const result = await executionService.execObservable(executionInfo, options, resource); + + assert.deepEqual(result, observable); + verify(processService.execObservable(executionInfo.execPath!, executionInfo.args, anything())).once(); + }); + + test('When calling exec, throw an error if environment variables are passed to the options parameter', () => { + const options = { env: { envOne: 'envOne' } }; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + moduleName: 'moduleOne', + args: ['-a', 'b', '-c'], + }; + + const promise = executionService.exec(executionInfo, options, resource); + + expect(promise).to.eventually.be.rejectedWith('Environment variables are not supported'); + }); + + test('When calling exec, use a python execution service if a module name is passed to the execution info', async () => { + const options = {}; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + moduleName: 'moduleOne', + args: ['-a', 'b', '-c'], + }; + + const result = await executionService.exec(executionInfo, options, resource); + + assert.deepEqual(result, executionResult); + verify(pythonService.execModule(executionInfo.moduleName!, executionInfo.args, options)).once(); + }); + + test('When calling exec, use a process service if an empty module name string is passed to the execution info', async () => { + const options = {}; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + moduleName: '', + args: ['-a', 'b', '-c'], + }; + + const result = await executionService.exec(executionInfo, options, resource); + + assert.deepEqual(result, executionResult); + verify(processService.exec(executionInfo.execPath!, executionInfo.args, anything())).once(); + }); + + test('When calling exec, use a process service if no module name is passed to the execution info', async () => { + const options = {}; + const executionInfo: ExecutionInfo = { + execPath: 'foo', + args: ['-a', 'b', '-c'], + }; + + const result = await executionService.exec(executionInfo, options, resource); + + assert.deepEqual(result, executionResult); + verify(processService.exec(executionInfo.execPath!, executionInfo.args, anything())).once(); + }); +}); diff --git a/src/test/common/process/serviceRegistry.unit.test.ts b/src/test/common/process/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..a0187aeedffc --- /dev/null +++ b/src/test/common/process/serviceRegistry.unit.test.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +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 { + IProcessServiceFactory, + IPythonExecutionFactory, + IPythonToolExecutionService, +} from '../../../client/common/process/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; + +suite('Common Process Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager)); + verify( + serviceManager.addSingleton<IProcessServiceFactory>(IProcessServiceFactory, ProcessServiceFactory), + ).once(); + verify( + serviceManager.addSingleton<IPythonExecutionFactory>(IPythonExecutionFactory, PythonExecutionFactory), + ).once(); + verify( + serviceManager.addSingleton<IPythonToolExecutionService>( + IPythonToolExecutionService, + PythonToolExecutionService, + ), + ).once(); + }); +}); diff --git a/src/test/common/randomWords.txt b/src/test/common/randomWords.txt new file mode 100644 index 000000000000..56066eaa9576 --- /dev/null +++ b/src/test/common/randomWords.txt @@ -0,0 +1,2000 @@ +screw +passenger +zesty +concerned +rustic +store +disagreeable +own +tranquil +modern +tickle +ceaseless +responsible +exclusive +harass +book +attach +squeak +amount +describe +deer +burst +women +influence +undesirable +jewel +inject +balance +dysfunctional +dog +recess +caption +abusive +hallowed +fabulous +maniacal +sweltering +adventurous +glorious +shut +carpenter +sun +kneel +impartial +ashamed +joke +therapeutic +friendly +wood +comfortable +repeat +pencil +agonizing +pricey +territory +scream +shrill +fry +invite +color +strange +zippy +plate +exist +succinct +wholesale +macabre +jam +cloudy +design +stone +apologise +snotty +ruddy +penitent +ban +eager +marry +neat +stale +angry +historical +park +club +cumbersome +table +kitty +parsimonious +sidewalk +dress +truck +ants +odd +worry +roll +stupid +jeans +desert +drop +nod +disastrous +gate +dreary +twist +plane +sky +piquant +naughty +complete +house +add +fool +hate +owe +stuff +humorous +kill +strip +dust +bump +moldy +separate +chalk +fly +third +guarded +sand +three +structure +tease +dispensable +beneficial +comb +attack +undress +bath +scarecrow +gusty +incredible +quaint +dream +wait +rainy +accept +tan +brass +sad +delay +ducks +joyous +trucks +tidy +redundant +unpack +square +north +belligerent +enthusiastic +utopian +last +zinc +shoe +reminiscent +offbeat +army +help +ear +draconian +religion +spark +yarn +spotty +moaning +polish +bite-sized +crayon +mess up +smile +endurable +nut +pedal +root +synonymous +complete +rotten +obedient +flippant +potato +twist +gratis +fresh +vague +slim +empty +grain +uttermost +warm +violet +harm +dad +crack +strap +animated +detect +aback +death +jail +announce +spooky +watch +wonder +unbecoming +zealous +gentle +quiver +royal +shade +attractive +crazy +live +courageous +zoo +solid +rice +applaud +willing +leather +friend +permit +plant +destroy +typical +tight +change +rabbit +behavior +oil +eyes +malicious +axiomatic +exercise +lunchroom +rod +spot +different +delightful +tire +ragged +juicy +tacky +corn +painstaking +tangible +gigantic +ground +curved +ablaze +messy +thick +truculent +paste +mellow +bashful +recognise +join +pull +obsolete +name +price +mixed +overwrought +plan +lick +five +creature +protect +daily +frequent +cynical +icicle +lock +insidious +rough +grubby +credit +challenge +descriptive +wet +introduce +notice +boil +zip +stop +gamy +star +wine +slap +measure +impossible +realise +concentrate +swim +drink +texture +calm +run +rhetorical +whine +page +mark +confused +ill-informed +diligent +good +ball +pause +befitting +toothbrush +bee +fancy +flower +elegant +rule +deafening +heartbreaking +purple +temper +scrape +plant +number +drain +arm +youthful +shame +snow +chop +event +advertisement +wiry +bikes +bat +mate +coach +nifty +parallel +degree +romantic +wanting +battle +meaty +full +unit +blade +wrestle +hook +wakeful +foolish +place +gaze +precede +volatile +replace +chivalrous +adjustment +idea +agree +eye +skinny +reward +grandfather +apparatus +pat +private +square +chief +brick +bomb +bulb +melt +form +snails +tent +giant +treatment +pail +shape +spiky +thoughtful +mean +disillusioned +sophisticated +lively +murky +tank +needle +harbor +gaping +subdued +momentous +dirty +married +secretive +frightened +easy +consider +scarce +absorbed +hammer +icky +metal +stocking +pathetic +son +car +crook +stiff +look +familiar +quirky +numerous +calendar +green +aunt +aromatic +air +complex +reply +health +current +observation +nation +burly +cannon +regret +listen +rings +door +level +sniff +unsightly +alert +doctor +false +desire +support +hammer +maddening +tasteless +secretary +special +earthy +argue +connection +hurry +smell +tour +cows +room +string +placid +confess +true +hypnotic +meal +caring +sore +swift +cup +fretful +peep +stay +scandalous +disarm +leg +material +arrange +strong +man +scorch +swing +society +quiet +peace +dynamic +flowers +helpful +breath +young +kindhearted +wind +glossy +knot +cooing +vegetable +idiotic +aboard +nutty +near +claim +bite +trick +preserve +mountainous +imagine +uninterested +enter +rat +gainful +prickly +coach +alluring +money +mouth +sip +fear +plantation +conscious +unequaled +jaded +appreciate +shivering +bake +weigh +labored +feigned +straight +end +act +consist +victorious +mountain +sleep +dinosaurs +trip +grate +last +regular +tiresome +whistle +gather +maid +pretend +weather +abandoned +drag +enjoy +foregoing +glove +boundary +weight +smelly +tug +butter +fit +manage +unarmed +steady +sassy +depressed +secret +abject +loud +fear +government +blood +alive +soak +wicked +bright +note +touch +innate +walk +jump +use +supreme +suspect +haunt +lethal +needy +seashore +colour +available +curious +brush +loose +cellar +push +evanescent +trace +cultured +ubiquitous +plot +wild +seemly +enchanting +milky +cure +lake +hospital +evasive +puzzling +woozy +lowly +acoustics +wax +madly +distance +bare +jump +van +decision +cheese +suggest +salt +houses +bury +spray +value +woman +raise +nest +fortunate +pass +efficient +stretch +interest +tray +pop +bounce +aspiring +busy +enormous +porter +yawn +frogs +immense +water +late +size +man +instinctive +whistle +skin +pot +zany +surprise +bubble +seal +nervous +lunch +combative +dazzling +feeble +enchanted +analyse +unique +provide +great +high-pitched +scare +arrive +push +signal +business +approve +steel +eight +common +windy +marked +sloppy +warm +fair +remain +sigh +knowing +frog +oceanic +tub +spectacular +knot +prepare +cover +tremendous +silent +stitch +shock +moon +calculate +representative +gleaming +dramatic +top +freezing +inquisitive +round +knife +oval +pack +fairies +taboo +ad hoc +abundant +unadvised +verse +condemned +tall +tap +confuse +sleet +peck +long +holiday +veil +lucky +produce +cautious +yoke +dear +industrious +present +political +mix +rejoice +lively +river +serve +cars +lush +zebra +loutish +sink +flavor +finger +flowery +yellow +marble +jealous +clip +dashing +pleasant +likeable +difficult +scene +quilt +forgetful +devilish +point +acrid +awake +imaginary +trouble +excuse +mourn +hat +town +puzzled +null +warlike +real +toothpaste +sleepy +lopsided +clumsy +uneven +lonely +harmonious +hospitable +temporary +avoid +trade +rock +deadpan +stranger +request +acidic +bone +actor +chilly +wheel +tie +rub +wall +chew +grab +clear +splendid +ghost +attraction +board +tip +aquatic +shop +orange +agreeable +branch +glass +line +increase +hulking +order +decorous +basketball +spot +monkey +cloistered +dust +ocean +scientific +camp +minor +skillful +worm +mom +divergent +pick +meeting +turn +expert +holistic +grey +sort +blushing +tart +touch +rainstorm +crush +field +upbeat +tangy +wish +handy +spotless +steep +afford +salty +snobbish +groan +uptight +question +drain +glistening +discover +wash +unkempt +funny +waste +fix +poison +allow +decisive +arrogant +robust +elastic +turkey +red +calculating +edge +old-fashioned +inexpensive +ticket +route +wail +dry +basin +squeamish +frighten +cart +elderly +murder +lavish +best +brown +glow +slave +tail +towering +scale +flame +alert +please +slope +direful +cactus +second-hand +macho +prevent +release +quarter +excite +party +trousers +test +defeated +long +throne +irritating +zoom +chase +afternoon +suffer +train +balance +station +force +smile +elfin +breakable +cheat +toes +tame +cute +medical +shallow +well-to-do +flimsy +smoke +blue-eyed +cloth +notebook +lettuce +ray +low +productive +wobble +existence +cow +wink +energetic +disappear +boy +lamp +distinct +illegal +addicted +aboriginal +yam +clean +black +hate +plug +comparison +rush +next +volleyball +distribution +sneaky +concern +precious +self +building +wound +psychotic +flap +same +swanky +quixotic +jail +oven +jumbled +past +spill +home +drown +impulse +imminent +sweet +helpless +didactic +turn +savory +ski +opposite +plant +stop +mint +wandering +appliance +repair +fluffy +eminent +lewd +physical +pig +regret +stomach +extra-small +stain +ugliest +cake +separate +partner +water +ring +end +envious +futuristic +itch +rifle +unite +puny +horn +reading +embarrassed +halting +channel +watery +wonderful +gruesome +point +shocking +relax +subtract +economic +luxuriant +parcel +radiate +wary +rich +stare +wasteful +six +nasty +quick +creepy +highfalutin +spotted +cobweb +explode +subsequent +blink +activity +plough +report +rabid +eggs +bouncy +quarrelsome +produce +street +spy +frantic +steadfast +strengthen +head +sour +unused +matter +jazzy +slow +tearful +nebulous +accidental +wool +impolite +simplistic +quicksand +spoil +meat +intelligent +scary +scarf +permissible +command +kettle +grade +animal +purring +crash +annoying +vein +duck +elbow +step +slippery +juvenile +war +ignorant +fuel +pigs +smash +peaceful +astonishing +lock +questionable +obtainable +stupendous +income +hour +love +sick +rate +compete +bent +servant +melted +blind +thing +obeisant +flesh +coherent +wooden +arch +cause +flow +understood +earn +gifted +wave +straw +skirt +boring +extra-large +daffy +detailed +tired +dogs +blue +possible +fish +makeshift +attack +bang +peel +magic +debonair +receive +orange +wise +ill-fated +striped +nail +belief +furniture +group +motionless +sugar +surround +tested +coal +question +well-off +squeal +waggish +wrist +actually +trains +bruise +show +ill +equal +earth +volcano +rambunctious +unusual +pocket +language +thought +parched +camera +pastoral +aggressive +land +learn +hurried +quizzical +bit +ignore +front +fade +discussion +mice +ambitious +abaft +suit +various +ink +dance +reduce +screeching +apparel +delicate +faithful +decide +low +staking +probable +curve +delicious +trade +drab +steer +argument +collect +sheep +anxious +search +brake +card +squealing +sprout +amazing +tree +farm +narrow +tense +books +gullible +alike +pumped +melodic +satisfying +shop +improve +button +irate +big +thin +drop +curl +umbrella +talk +marvelous +level +stir +tenuous +yummy +arithmetic +overt +pull +gray +large +rule +teeny-tiny +shelter +scared +judge +wacky +cakes +merciful +testy +shave +limping +power +abiding +bless +dapper +internal +division +taste +donkey +airplane +dolls +ethereal +spiteful +smoke +bear +knowledgeable +like +delight +picayune +toe +apathetic +wealthy +sponge +sail +crow +slip +loss +weak +pointless +queen +ship +letters +pollution +upset +aberrant +yard +bumpy +pin +pushy +waste +expand +vacuous +fierce +determined +discreet +lip +paint +stingy +vest +amusing +two +nappy +hungry +wilderness +offer +kindly +connect +employ +neighborly +dare +open +planes +cat +office +voyage +float +festive +cracker +adaptable +ludicrous +omniscient +guiltless +heavenly +even +name +appear +crowded +homely +kaput +stick +spiffy +classy +disgusting +heat +thirsty +nimble +invincible +shiny +paper +songs +able +tasteful +open +mighty +chemical +trot +flag +sincere +wren +known +tempt +afraid +squirrel +exultant +ordinary +quill +sound +thunder +haircut +lame +beef +airport +cut +vigorous +boat +prefer +disagree +race +bubble +sore +famous +baby +accessible +tumble +callous +whirl +rob +lackadaisical +view +bike +seed +mother +jar +used +risk +move +yell +groovy +vast +protest +normal +wide-eyed +paddle +bell +charming +nerve +delirious +overconfident +teeny +choke +pleasure +elite +capricious +sin +snore +mine +lie +call +resolute +bathe +dry +dock +careful +program +birds +mere +neck +second +scratch +spiritual +little +x-ray +greasy +cattle +ripe +property +snakes +crooked +aware +cooperative +plastic +observant +expansion +sedate +class +geese +first +industry +knee +change +hard-to-find +intend +icy +scent +obsequious +hum +form +happy +relation +detail +person +science +reign +addition +shade +possess +mysterious +sister +teeth +remember +telling +outstanding +repulsive +soothe +succeed +scrub +rebel +morning +crawl +hobbies +alleged +middle +old +absurd +nose +polite +anger +erratic +part +memory +alcoholic +picture +vanish +small +fire +mass +obscene +tendency +daughter +decay +drunk +rain +muddle +sudden +hover +pen +poor +embarrass +judge +carriage +cool +land +cheap +error +damage +periodic +thumb +guitar +engine +waiting +fertile +unaccountable +correct +fetch +skip +base +educate +nonchalant +racial +double +continue +painful +type +cave +steam +roasted +clean +cycle +borrow +rapid +automatic +bait +tin +saw +development +walk +suggestion +judicious +time +bird +clap +deeply +inconclusive +vulgar +cast +sneeze +bleach +nosy +explain +settle +military +trashy +ruthless +cemetery +book +cluttered +pets +unable +mark +thoughtless +fork +thankful +foamy +seat +smell +writing +eggnog +care +shaky +breezy +unruly +lying +chunky +hope +brother +shirt +panoramic +truthful +education +condition +psychedelic +extend +deliver +miniature +rain +oatmeal +voiceless +hot +mammoth +finger +empty +smart +guide +direction +gorgeous +position +friends +trap +zonked +oranges +adhesive +order +boundless +public +telephone +fascinated +noxious +rhythm +zephyr +tongue +organic +tense +knowledge +fold +vengeful +authority +faulty +head +dusty +bow +ambiguous +sneeze +broken +sharp +spell +poised +egg +fragile +stamp +company +load +ancient +somber +believe +fearless +thread +kick +compare +beam +interest +sordid +hard +infamous +impress +earthquake +action +ready +superficial +contain +spring +colorful +humdrum +certain +tricky +bitter +scatter +laugh +greedy +silly +join +prick +four +crate +jittery +bead +giraffe +whip +kick +needless +rinse +rot +history +roll +boot +hellish +instrument +object +lovely +tame +trite +majestic +rescue +superb +ten +frail +stage +spicy +crib +brake +pies +sign +flood +gun +trust +preach +ugly +abrupt +unhealthy +wave +drawer +grass +bloody +shock +hanging +versed +window +workable +suit +sulky +mindless +few +disgusted +achiever +art +verdant +lacking +flagrant +materialistic +grandmother +frame +save +thrill +tiny +reflect +nonstop +jog +wrathful +advise +righteous +massive +numberless +magnificent +cheerful +left +protective +talk +lace +nauseating +fearful +month +obnoxious +selfish +soda +plain +meddle +can +absorbing +rock +hollow +weary +cable +beautiful +awesome +glib +harmony +frightening +ladybug +occur +abhorrent +dress +powder +example +carry +experience +dizzy +noise +mushy +baseball +cross +jelly +heavy +hose +entertaining +store +moan +ahead +changeable +unknown +drum +hand +pale +mature +work +grip +control +grape +jam +sweater +nippy +muddled +lazy +whole +useless +start +fast +advice +simple +want +tremble +many +learned +terrific +bag +symptomatic +pray +tiger +outrageous +theory +resonant +sack +hushed +hysterical +match +care +support +cabbage +beginner +committee +voracious +spurious +miss +silky +profit +whisper +noisy +thundering +horse +tacit +sail +scissors +thaw +domineering +trouble +box +discovery +childlike +cuddly +perpetual +husky +fruit +scold +elated +godly +guarantee +nutritious +hesitant +doubt +cherries +curly +cough +move +bottle +clear +ratty +stretch +stormy +overflow +puffy +tick +harsh +female +test +illustrious +expensive +muscle +attend +stereotyped +payment +deep +afterthought +pear +quiet +launch +suppose +examine +worried +selective +flower +motion +divide +wriggle +warn +flashy +hateful +milk +hideous +post +unbiased +rural +remind +transport +fancy +list +day +reaction +thinkable +absent +grieving +increase +cream +thank +interrupt +bewildered +aftermath +misty +mind +grease +cover +overjoyed +develop +deceive +growth +treat +complain +pine +wish +twig +box +heady +hall +previous +liquid +aloof +dull +trees +present +wipe +key +jobless +careless +week +mute +curvy +imported +need +puncture +whip +title +finicky +pancake +unwritten +suck +acceptable +valuable +play +quack +wretched +magenta +shoes +wry +vacation +deserve +coil +grotesque +wide +fixed +womanly +rare +wire +heap +badge +honorable +irritate +bawdy +supply +sheet +erect +frame +hilarious +colossal +bed +girl +pet +crabby +cry +deranged +wistful +plucky +pump +cold +shake +satisfy +safe +handsomely +faded +follow +serious +dangerous +insect +annoy +loaf +soap +taste +mitten +lyrical +substantial +fog +wrench +destruction +lighten +wrap +soggy +hot +terrible +bedroom +fanatical +receipt diff --git a/src/test/common/serviceRegistry.unit.test.ts b/src/test/common/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..9a82681625d4 --- /dev/null +++ b/src/test/common/serviceRegistry.unit.test.ts @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { ActiveResourceService } from '../../client/common/application/activeResource'; +import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; +import { ApplicationShell } from '../../client/common/application/applicationShell'; +import { CommandManager } from '../../client/common/application/commandManager'; +import { DebugService } from '../../client/common/application/debugService'; +import { DocumentManager } from '../../client/common/application/documentManager'; +import { Extensions } from '../../client/common/application/extensions'; +import { LanguageService } from '../../client/common/application/languageService'; +import { TerminalManager } from '../../client/common/application/terminalManager'; +import { + IActiveResourceService, + IApplicationEnvironment, + IApplicationShell, + ICommandManager, + IDebugService, + IDocumentManager, + ILanguageService, + ITerminalManager, + IWorkspaceService, +} from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { ConfigurationService } from '../../client/common/configuration/service'; +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 { PersistentStateFactory } from '../../client/common/persistentState'; +import { PathUtils } from '../../client/common/platform/pathUtils'; +import { CurrentProcess } from '../../client/common/process/currentProcess'; +import { registerTypes } from '../../client/common/serviceRegistry'; +import { TerminalActivator } from '../../client/common/terminal/activator'; +import { PowershellTerminalActivationFailedHandler } from '../../client/common/terminal/activator/powershellFailedHandler'; +import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { 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'; +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 { + IBrowserService, + IConfigurationService, + ICurrentProcess, + IExtensions, + IInstaller, + IInterpreterPathService, + IPathUtils, + IPersistentStateFactory, + IRandom, + IToolExecutionPath, + ToolExecutionPath, +} from '../../client/common/types'; +import { IMultiStepInputFactory, MultiStepInputFactory } from '../../client/common/utils/multiStepInput'; +import { Random } from '../../client/common/utils/random'; +import { IServiceManager } from '../../client/ioc/types'; +import { ImportTracker } from '../../client/telemetry/importTracker'; +import { IImportTracker } from '../../client/telemetry/types'; + +suite('Common - Service Registry', () => { + test('Registrations', () => { + const serviceManager = typemoq.Mock.ofType<IServiceManager>(); + + [ + [IActiveResourceService, ActiveResourceService], + [IInterpreterPathService, InterpreterPathService], + [IExtensions, Extensions], + [IRandom, Random], + [IPersistentStateFactory, PersistentStateFactory], + [ITerminalServiceFactory, TerminalServiceFactory], + [IPathUtils, PathUtils], + [IApplicationShell, ApplicationShell], + [ICurrentProcess, CurrentProcess], + [IInstaller, ProductInstaller], + [ICommandManager, CommandManager], + [IConfigurationService, ConfigurationService], + [IWorkspaceService, WorkspaceService], + [IDocumentManager, DocumentManager], + [ITerminalManager, TerminalManager], + [IDebugService, DebugService], + [IApplicationEnvironment, ApplicationEnvironment], + [ILanguageService, LanguageService], + [IBrowserService, BrowserService], + [ITerminalActivator, TerminalActivator], + [ITerminalActivationHandler, PowershellTerminalActivationFailedHandler], + [ITerminalHelper, TerminalHelper], + [ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, TerminalActivationProviders.pyenv], + [ITerminalActivationCommandProvider, Bash, TerminalActivationProviders.bashCShellFish], + [ + ITerminalActivationCommandProvider, + CommandPromptAndPowerShell, + TerminalActivationProviders.commandPromptAndPowerShell, + ], + [ITerminalActivationCommandProvider, Nushell, TerminalActivationProviders.nushell], + [IToolExecutionPath, PipEnvExecutionPath, ToolExecutionPath.pipenv], + [ITerminalActivationCommandProvider, CondaActivationCommandProvider, TerminalActivationProviders.conda], + [ITerminalActivationCommandProvider, PipEnvActivationCommandProvider, TerminalActivationProviders.pipenv], + [IMultiStepInputFactory, MultiStepInputFactory], + [IImportTracker, ImportTracker], + [IShellDetector, TerminalNameShellDetector], + [IShellDetector, SettingsShellDetector], + [IShellDetector, UserEnvironmentShellDetector], + [IShellDetector, VSCEnvironmentShellDetector], + ].forEach((mapping) => { + if (mapping.length === 2) { + serviceManager + .setup((s) => + s.addSingleton( + typemoq.It.isValue(mapping[0] as any), + typemoq.It.is((value: any) => mapping[1] === value), + ), + ) + .verifiable(typemoq.Times.atLeastOnce()); + } else { + serviceManager + .setup((s) => + s.addSingleton( + typemoq.It.isValue(mapping[0] as any), + typemoq.It.isAny(), + typemoq.It.isValue(mapping[2] as any), + ), + ) + .callback((_, cls) => expect(cls).to.equal(mapping[1])) + .verifiable(typemoq.Times.once()); + } + }); + + registerTypes(serviceManager.object); + serviceManager.verifyAll(); + }); +}); diff --git a/src/test/common/socketCallbackHandler.test.ts b/src/test/common/socketCallbackHandler.test.ts index cb90545b241d..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,8 +33,11 @@ class MockSocketCallbackHandler extends SocketCallbackHandler { public ping(message: string) { this.SendRawCommand(Commands.PingBytes); - const stringBuffer = new Buffer(message); - const buffer = Buffer.concat([Buffer.concat([new Buffer('U'), uint64be.encode(stringBuffer.byteLength)]), stringBuffer]); + const stringBuffer = Buffer.from(message); + const buffer = Buffer.concat([ + Buffer.concat([Buffer.from('U'), uint64be.encode(stringBuffer.byteLength)]), + stringBuffer, + ]); this.stream.Write(buffer); } protected handleHandshake(): boolean { @@ -86,7 +86,7 @@ class MockSocketClient { private socket?: net.Socket; private socketStream?: SocketStream; private def?: Deferred<any>; - constructor(private port: number) { } + constructor(private port: number) {} public get SocketStream(): SocketStream { if (this.socketStream === undefined) { throw Error('not listening'); @@ -95,16 +95,16 @@ class MockSocketClient { } public start(): Promise<any> { this.def = createDeferred<any>(); - this.socket = net.connect(this.port, this.connectionListener.bind(this)); + this.socket = net.connect(this.port as any, this.connectionListener.bind(this)); return this.def.promise; } private connectionListener() { if (this.socket === undefined || this.def === undefined) { throw Error('not started'); } - this.socketStream = new SocketStream(this.socket, new Buffer('')); + this.socketStream = new SocketStream(this.socket, Buffer.from('')); this.def.resolve(); - this.socket.on('error', () => { }); + this.socket.on('error', () => {}); this.socket.on('data', (data: Buffer) => { try { this.SocketStream.Append(data); @@ -119,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(); @@ -129,24 +129,33 @@ 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)]); + const errorBuffer = Buffer.concat([ + 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 pongBuffer = Buffer.concat([Buffer.concat([new Buffer('U'), uint64be.encode(messageBuffer.byteLength)]), messageBuffer]); + const messageBuffer = Buffer.from(message); + const pongBuffer = Buffer.concat([ + 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 errorBuffer = Buffer.concat([Buffer.concat([new Buffer('A'), uint64be.encode(errorMessage.length)]), new Buffer(errorMessage)]); + const errorMessage = `Fatal error in handling data at socket client. Error: ${(ex as Error).message}`; + const errorBuffer = Buffer.concat([ + Buffer.concat([Buffer.from('A'), uint64be.encode(errorMessage.length)]), + Buffer.from(errorMessage), + ]); this.SocketStream.Write(errorBuffer); } }); @@ -156,7 +165,7 @@ class MockSocketClient { // Defines a Mocha test suite to group tests of similar kind together suite('SocketCallbackHandler', () => { let socketServer: SocketServer; - setup(() => socketServer = new SocketServer()); + setup(() => (socketServer = new SocketServer())); teardown(() => socketServer.Stop()); test('Succesfully starts without any specific host or port', async () => { @@ -180,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); }); @@ -201,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; @@ -213,20 +222,20 @@ suite('SocketCallbackHandler', () => { await socketClient.start(); const def = createDeferred<any>(); - let timeOut: NodeJS.Timer | undefined = setTimeout(() => { + let timeOut: NodeJS.Timer | undefined | number = setTimeout(() => { def.reject('Handshake not completed in allocated time'); }, 5000); callbackHandler.on('handshake', () => { if (timeOut) { - clearTimeout(timeOut); + clearTimeout(timeOut as any); timeOut = undefined; } def.reject('handshake should fail, but it succeeded!'); }); callbackHandler.on('error', (actual: string | number, expected: string, message: string) => { if (timeOut) { - clearTimeout(timeOut); + clearTimeout(timeOut as any); timeOut = undefined; } if (actual === 0 && message === 'pids not the same') { @@ -237,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 @@ -262,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) => { @@ -272,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 @@ -295,13 +304,15 @@ 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)); + const availablePort = await new Promise<number>((resolve, reject) => + getFreePort.default({ host: 'localhost' }).then(resolve, reject), + ); const port = await socketServer.Start({ port: availablePort, host: 'localhost' }); expect(port).to.be.equal(availablePort, 'Server is not listening on the provided port number'); @@ -319,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 3cae966392f7..35420e4a614c 100644 --- a/src/test/common/socketStream.test.ts +++ b/src/test/common/socketStream.test.ts @@ -4,167 +4,189 @@ // // Place this right on top -import { initialize } from './../initialize'; // The module 'assert' provides assertion methods from node import * as assert from 'assert'; // You can import and use all API from the 'vscode' module // as well as import your extension to test it -import * as vscode from 'vscode'; -import { SocketStream } from '../../client/common/net/socket/SocketStream'; import * as net from 'net'; -const uint64be = require("uint64be"); +import { SocketStream } from '../../client/common/net/socket/SocketStream'; + +const uint64be = require('uint64be'); class MockSocket { + private _data: string; + + private _rawDataWritten: any; constructor() { this._data = ''; } - private _data: string; - private _rawDataWritten: any; public get dataWritten(): string { return this._data; } + public get rawDataWritten(): any { return this._rawDataWritten; } - write(data: any) { - this._data = data + ''; + + public write(data: any) { + this._data = `${data}` + ''; this._rawDataWritten = data; } } // Defines a Mocha test suite to group tests of similar kind together + suite('SocketStream', () => { - test('Read Byte', done => { - let buffer = new Buffer("X"); + test('Read Byte', (done) => { + const buffer = Buffer.from('X'); const byteValue = buffer[0]; const socket = new MockSocket(); - const stream = new SocketStream((socket as any) as net.Socket, buffer) - assert.equal(stream.ReadByte(), byteValue); + const stream = new SocketStream((socket as any) as net.Socket, buffer); + + assert.strictEqual(stream.ReadByte(), byteValue); done(); }); - test('Read Int32', done => { + test('Read Int32', (done) => { const num = 1234; const socket = new MockSocket(); - let buffer = uint64be.encode(num); - const stream = new SocketStream((socket as any) as net.Socket, buffer) + const buffer = uint64be.encode(num); - assert.equal(stream.ReadInt32(), num); + const stream = new SocketStream((socket as any) as net.Socket, buffer); + + assert.strictEqual(stream.ReadInt32(), num); done(); }); - test('Read Int64', done => { + test('Read Int64', (done) => { const num = 9007199254740993; const socket = new MockSocket(); - let buffer = uint64be.encode(num); - const stream = new SocketStream((socket as any) as net.Socket, buffer) + const buffer = uint64be.encode(num); + + 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 => { + test('Read Ascii String', (done) => { const message = 'Hello World'; const socket = new MockSocket(); - let buffer = Buffer.concat([new Buffer('A'), uint64be.encode(message.length), new Buffer(message)]); - const stream = new SocketStream((socket as any) as net.Socket, buffer) + 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 => { + test('Read Unicode String', (done) => { const message = 'Hello World - Функция проверки ИНН и КПП - 说明'; const socket = new MockSocket(); - const stringBuffer = new Buffer(message); - let buffer = Buffer.concat([Buffer.concat([new Buffer('U'), uint64be.encode(stringBuffer.byteLength)]), stringBuffer]); - const stream = new SocketStream((socket as any) as net.Socket, buffer) + const stringBuffer = Buffer.from(message); + const buffer = Buffer.concat([ + Buffer.concat([Buffer.from('U'), uint64be.encode(stringBuffer.byteLength)]), + stringBuffer, + ]); - assert.equal(stream.ReadString(), message); + const stream = new SocketStream((socket as any) as net.Socket, buffer); + + assert.strictEqual(stream.ReadString(), message); done(); }); - test('Read RollBackTransaction', 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]); - const stream = new SocketStream((socket as any) as net.Socket, buffer) + + const stream = new SocketStream((socket as any) as net.Socket, buffer); stream.BeginTransaction(); - assert.equal(stream.ReadString(), message, 'First message not read properly'); - const secondMessage = stream.ReadString(); - assert.equal(stream.HasInsufficientDataForReading, true, 'Should not have sufficient data for reading'); + assert.strictEqual(stream.ReadString(), message, 'First message not read properly'); + stream.ReadString(); + 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 => { + 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]); - const stream = new SocketStream((socket as any) as net.Socket, buffer) + + const stream = new SocketStream((socket as any) as net.Socket, buffer); stream.BeginTransaction(); - assert.equal(stream.ReadString(), message, 'First message not read properly'); - const secondMessage = stream.ReadString(); - assert.equal(stream.HasInsufficientDataForReading, true, 'Should not have sufficient data for reading'); + assert.strictEqual(stream.ReadString(), message, 'First message not read properly'); + stream.ReadString(); + 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 => { + test('Write Buffer', (done) => { const message = 'Hello World'; - const buffer = new Buffer(''); + const buffer = Buffer.from(''); const socket = new MockSocket(); - const stream = new SocketStream((socket as any) as net.Socket, buffer) - stream.Write(new Buffer(message)); - assert.equal(socket.dataWritten, message) + const stream = new SocketStream((socket as any) as net.Socket, buffer); + stream.Write(Buffer.from(message)); + + assert.strictEqual(socket.dataWritten, message); done(); }); - test('Write Int32', done => { + test('Write Int32', (done) => { const num = 1234; - const buffer = new Buffer(''); + const buffer = Buffer.from(''); const socket = new MockSocket(); - const stream = new SocketStream((socket as any) as net.Socket, buffer) + + 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 => { + test('Write Int64', (done) => { const num = 9007199254740993; - const buffer = new Buffer(''); + const buffer = Buffer.from(''); const socket = new MockSocket(); - const stream = new SocketStream((socket as any) as net.Socket, buffer) + + 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 => { + test('Write Ascii String', (done) => { const message = 'Hello World'; - const buffer = new Buffer(''); + const buffer = Buffer.from(''); const socket = new MockSocket(); - const stream = new SocketStream((socket as any) as net.Socket, buffer) + + 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 => { + test('Write Unicode String', (done) => { const message = 'Hello World - Функция проверки ИНН и КПП - 说明'; - const buffer = new Buffer(''); + const buffer = Buffer.from(''); const socket = new MockSocket(); - const stream = new SocketStream((socket as any) as net.Socket, buffer) + + 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 0fafb57a0277..cd057e7be3e5 100644 --- a/src/test/common/terminals/activation.bash.unit.test.ts +++ b/src/test/common/terminals/activation.bash.unit.test.ts @@ -8,33 +8,50 @@ 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'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; -// tslint:disable-next-line:max-func-body-length suite('Terminal Environment Activation (bash)', () => { - ['usr/bin/python', 'usr/bin/env with spaces/env more/python', 'c:\\users\\windows paths\\conda\\python.exe'].forEach(pythonPath => { + [ + 'usr/bin/python', + 'usr/bin/env with spaces/env more/python', + 'c:\\users\\windows paths\\conda\\python.exe', + ].forEach((pythonPath) => { const hasSpaces = pythonPath.indexOf(' ') > 0; - const suiteTitle = hasSpaces ? 'and there are spaces in the script file (pythonpath),' : 'and there are no spaces in the script file (pythonpath),'; + 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 => { + [ + '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); + 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 => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((shellType) => { let isScriptFileSupported = false; switch (shellType.value) { case TerminalShellType.zsh: @@ -58,8 +75,9 @@ suite('Terminal Environment Activation (bash)', () => { isScriptFileSupported = false; } } - const titleTitle = isScriptFileSupported ? `Ensure bash Activation command returns activation command (Shell: ${shellType.name})` : - `Ensure bash Activation command returns undefined (Shell: ${shellType.name})`; + 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); @@ -74,18 +92,26 @@ suite('Terminal Environment Activation (bash)', () => { case TerminalShellType.tcshell: case TerminalShellType.cshell: case TerminalShellType.fish: { - expect(supported).to.be.equal(true, `${shellType.name} shell not supported (it should be)`); + 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)`); + 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)); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); const command = await bash.getActivationCommands(undefined, shellType.value); if (isScriptFileSupported) { @@ -94,7 +120,10 @@ suite('Terminal Environment Activation (bash)', () => { // 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'); + 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 63e3fa23468f..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,38 +8,48 @@ 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)', () => { - ['c:/programfiles/python/python', 'c:/program files/python/python', - 'c:\\users\\windows paths\\conda\\python.exe'].forEach(pythonPath => { - const hasSpaces = pythonPath.indexOf(' ') > 0; - const resource = Uri.file('a'); - - const suiteTitle = hasSpaces ? 'and there are spaces in the script file (pythonpath),' : 'and there are no spaces in the script file (pythonpath),'; - suite(suiteTitle, () => { - ['activate', 'activate.sh', 'activate.csh', 'activate.fish', 'activate.bat', 'activate.ps1'].forEach(scriptFileName => { + let interpreterService: TypeMoq.IMock<IInterpreterService>; + [ + 'c:/programfiles/python/python', + 'c:/program files/python/python', + 'c:\\users\\windows paths\\conda\\python.exe', + ].forEach((pythonPath) => { + const hasSpaces = pythonPath.indexOf(' ') > 0; + const resource = Uri.file('a'); + + const suiteTitle = hasSpaces + ? 'and there are spaces in the script file (pythonpath),' + : 'and there are no spaces in the script file (pythonpath),'; + suite(suiteTitle, () => { + ['activate', 'activate.sh', 'activate.csh', 'activate.fish', 'activate.bat', 'Activate.ps1'].forEach( + (scriptFileName) => { suite(`and script file is ${scriptFileName}`, () => { let serviceContainer: TypeMoq.IMock<IServiceContainer>; let fileSystem: TypeMoq.IMock<IFileSystem>; setup(() => { serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - serviceContainer.setup(c => c.get(IFileSystem)).returns(() => fileSystem.object); - - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); - const settings = TypeMoq.Mock.ofType<IPythonSettings>(); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + 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); }); - getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(shellType => { - const isScriptFileSupported = ['activate.bat', 'activate.ps1'].indexOf(scriptFileName) >= 0; - const titleTitle = isScriptFileSupported ? `Ensure terminal type is supported (Shell: ${shellType.name})` : - `Ensure terminal type is not supported (Shell: ${shellType.name})`; + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((shellType) => { + const isScriptFileSupported = ['activate.bat', 'Activate.ps1'].indexOf(scriptFileName) >= 0; + const titleTitle = isScriptFileSupported + ? `Ensure terminal type is supported (Shell: ${shellType.name})` + : `Ensure terminal type is not supported (Shell: ${shellType.name})`; test(titleTitle, async () => { const bash = new CommandPromptAndPowerShell(serviceContainer.object); @@ -51,147 +59,180 @@ suite('Terminal Environment Activation (cmd/powershell)', () => { case TerminalShellType.commandPrompt: case TerminalShellType.powershellCore: case TerminalShellType.powershell: { - expect(supported).to.be.equal(true, `${shellType.name} shell not supported (it should be)`); + 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)`); + expect(supported).to.be.equal( + false, + `${shellType.name} incorrectly supported (should not be)`, + ); } } }); }); }); + }, + ); + + suite('and script file is activate.bat', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let platform: TypeMoq.IMock<IPlatformService>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + platform = TypeMoq.Mock.ofType<IPlatformService>(); + 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); }); - suite('and script file is activate.bat', () => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let fileSystem: TypeMoq.IMock<IFileSystem>; - let platform: TypeMoq.IMock<IPlatformService>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - platform = TypeMoq.Mock.ofType<IPlatformService>(); - serviceContainer.setup(c => c.get(IFileSystem)).returns(() => fileSystem.object); - serviceContainer.setup(c => c.get(IPlatformService)).returns(() => platform.object); - - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); - const settings = TypeMoq.Mock.ofType<IPythonSettings>(); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - }); - - test('Ensure batch files are supported by command prompt', async () => { - const bash = new CommandPromptAndPowerShell(serviceContainer.object); + test('Ensure batch files are supported by command prompt', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); - const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); - const commands = await bash.getActivationCommands(resource, TerminalShellType.commandPrompt); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const commands = await bash.getActivationCommands(resource, TerminalShellType.commandPrompt); - // Ensure the script file is of the following form: - // source "<path to script file>" <environment name> - // Ensure the path is quoted if it contains any spaces. - // Ensure it contains the name of the environment as an argument to the script file. + // Ensure the script file is of the following form: + // source "<path to script file>" <environment name> + // Ensure the path is quoted if it contains any spaces. + // Ensure it contains the name of the environment as an argument to the script file. - expect(commands).to.be.deep.equal([pathToScriptFile.fileToCommandArgument()], 'Invalid command'); - }); + expect(commands).to.be.deep.equal( + [pathToScriptFile.fileToCommandArgumentForPythonExt()], + 'Invalid command', + ); + }); - test('Ensure batch files are not supported by powershell (on windows)', async () => { - const batch = new CommandPromptAndPowerShell(serviceContainer.object); + test('Ensure batch files are not supported by powershell (on windows)', async () => { + const batch = new CommandPromptAndPowerShell(serviceContainer.object); - platform.setup(p => p.isWindows).returns(() => true); - const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); - const command = await batch.getActivationCommands(resource, TerminalShellType.powershell); + platform.setup((p) => p.isWindows).returns(() => true); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await batch.getActivationCommands(resource, TerminalShellType.powershell); - expect(command).to.be.equal(undefined, 'Invalid'); - }); + expect(command).to.be.equal(undefined, 'Invalid'); + }); - test('Ensure batch files are not supported by powershell core (on windows)', async () => { - const bash = new CommandPromptAndPowerShell(serviceContainer.object); + test('Ensure batch files are not supported by powershell core (on windows)', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); - platform.setup(p => p.isWindows).returns(() => true); - const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); - const command = await bash.getActivationCommands(resource, TerminalShellType.powershellCore); + platform.setup((p) => p.isWindows).returns(() => true); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands(resource, TerminalShellType.powershellCore); - expect(command).to.be.equal(undefined, 'Invalid'); - }); + expect(command).to.be.equal(undefined, 'Invalid'); + }); - test('Ensure batch files are not supported by powershell (on non-windows)', async () => { - const bash = new CommandPromptAndPowerShell(serviceContainer.object); + test('Ensure batch files are not supported by powershell (on non-windows)', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); - platform.setup(p => p.isWindows).returns(() => false); - const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); - const command = await bash.getActivationCommands(resource, TerminalShellType.powershell); + platform.setup((p) => p.isWindows).returns(() => false); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands(resource, TerminalShellType.powershell); - expect(command).to.be.equal(undefined, 'Invalid command'); - }); + expect(command).to.be.equal(undefined, 'Invalid command'); + }); - test('Ensure batch files are not supported by powershell core (on non-windows)', async () => { - const bash = new CommandPromptAndPowerShell(serviceContainer.object); + test('Ensure batch files are not supported by powershell core (on non-windows)', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); - platform.setup(p => p.isWindows).returns(() => false); - const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); - const command = await bash.getActivationCommands(resource, TerminalShellType.powershellCore); + platform.setup((p) => p.isWindows).returns(() => false); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.bat'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands(resource, TerminalShellType.powershellCore); - expect(command).to.be.equal(undefined, 'Invalid command'); - }); + expect(command).to.be.equal(undefined, 'Invalid command'); }); + }); - suite('and script file is activate.ps1', () => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let fileSystem: TypeMoq.IMock<IFileSystem>; - let platform: TypeMoq.IMock<IPlatformService>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - platform = TypeMoq.Mock.ofType<IPlatformService>(); - serviceContainer.setup(c => c.get(IFileSystem)).returns(() => fileSystem.object); - serviceContainer.setup(c => c.get(IPlatformService)).returns(() => platform.object); - - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); - const settings = TypeMoq.Mock.ofType<IPythonSettings>(); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - }); + suite('and script file is Activate.ps1', () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let platform: TypeMoq.IMock<IPlatformService>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + platform = TypeMoq.Mock.ofType<IPlatformService>(); + serviceContainer.setup((c) => c.get(IFileSystem)).returns(() => fileSystem.object); + serviceContainer.setup((c) => c.get(IPlatformService)).returns(() => platform.object); + 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 () => { - const bash = new CommandPromptAndPowerShell(serviceContainer.object); + test('Ensure powershell files are not supported by command prompt', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); - platform.setup(p => p.isWindows).returns(() => true); - const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.ps1'); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); - const command = await bash.getActivationCommands(resource, TerminalShellType.commandPrompt); + platform.setup((p) => p.isWindows).returns(() => true); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'Activate.ps1'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands(resource, TerminalShellType.commandPrompt); - expect(command).to.be.deep.equal([], 'Invalid command (running powershell files are not supported on command prompt)'); - }); + expect(command).to.be.deep.equal( + [], + 'Invalid command (running powershell files are not supported on command prompt)', + ); + }); - test('Ensure powershell files are supported by powershell', async () => { - const bash = new CommandPromptAndPowerShell(serviceContainer.object); + test('Ensure powershell files are supported by powershell', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); - platform.setup(p => p.isWindows).returns(() => true); - const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.ps1'); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); - const command = await bash.getActivationCommands(resource, TerminalShellType.powershell); + platform.setup((p) => p.isWindows).returns(() => true); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'Activate.ps1'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands(resource, TerminalShellType.powershell); - expect(command).to.be.deep.equal([`& ${pathToScriptFile.fileToCommandArgument()}`.trim()], 'Invalid command'); - }); + expect(command).to.be.deep.equal( + [`& ${pathToScriptFile.fileToCommandArgumentForPythonExt()}`.trim()], + 'Invalid command', + ); + }); - test('Ensure powershell files are supported by powershell core', async () => { - const bash = new CommandPromptAndPowerShell(serviceContainer.object); + test('Ensure powershell files are supported by powershell core', async () => { + const bash = new CommandPromptAndPowerShell(serviceContainer.object); - platform.setup(p => p.isWindows).returns(() => true); - const pathToScriptFile = path.join(path.dirname(pythonPath), 'activate.ps1'); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))).returns(() => Promise.resolve(true)); - const command = await bash.getActivationCommands(resource, TerminalShellType.powershellCore); + platform.setup((p) => p.isWindows).returns(() => true); + const pathToScriptFile = path.join(path.dirname(pythonPath), 'Activate.ps1'); + fileSystem + .setup((fs) => fs.fileExists(TypeMoq.It.isValue(pathToScriptFile))) + .returns(() => Promise.resolve(true)); + const command = await bash.getActivationCommands(resource, TerminalShellType.powershellCore); - expect(command).to.be.deep.equal([`& ${pathToScriptFile.fileToCommandArgument()}`.trim()], 'Invalid command'); - }); + expect(command).to.be.deep.equal( + [`& ${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 3a906d1b79c2..39bf58a9a36b 100644 --- a/src/test/common/terminals/activation.conda.unit.test.ts +++ b/src/test/common/terminals/activation.conda.unit.test.ts @@ -1,34 +1,37 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// tslint:disable:max-func-body-length no-any - import { expect } from 'chai'; import * as path from 'path'; -import { parse } from 'semver'; +import { anything, instance, mock, when } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; import { Disposable } from 'vscode'; +import { TerminalManager } from '../../../client/common/application/terminalManager'; import '../../../client/common/extensions'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { Bash } from '../../../client/common/terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from '../../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { Nushell } from '../../../client/common/terminal/environmentActivationProviders/nushell'; import { - IFileSystem, IPlatformService -} from '../../../client/common/platform/types'; -import { - IProcessService, IProcessServiceFactory -} from '../../../client/common/process/types'; -import { - CondaActivationCommandProvider + 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'; +import { ITerminalActivationCommandProvider, TerminalShellType } from '../../../client/common/terminal/types'; import { - ITerminalActivationCommandProvider, TerminalShellType -} from '../../../client/common/terminal/types'; -import { - IConfigurationService, IDisposableRegistry, - IPythonSettings, ITerminalSettings + IConfigurationService, + IDisposableRegistry, + IPythonSettings, + 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; @@ -41,280 +44,524 @@ 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; setup(() => { conda = 'conda'; serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); disposables = []; - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())).returns(() => disposables); + serviceContainer + .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)); + 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.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); - - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())).returns(() => platformService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())).returns(() => fileSystem.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())).returns(() => procServiceFactory.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICondaService), TypeMoq.It.isAny())).returns(() => condaService.object); - - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); + procServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())) + .returns(() => platformService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) + .returns(() => fileSystem.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())) + .returns(() => procServiceFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ICondaService), TypeMoq.It.isAny())) + .returns(() => condaService.object); + + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>(); - pythonSettings.setup(s => s.terminal).returns(() => terminalSettings.object); - - terminalHelper = new TerminalHelper(serviceContainer.object); + pythonSettings.setup((s) => s.terminal).returns(() => terminalSettings.object); + + terminalHelper = new TerminalHelper( + platformService.object, + instance(mock(TerminalManager)), + serviceContainer.object, + instance(mock(InterpreterService)), + 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(() => { - disposables.forEach(disposable => { + disposables.forEach((disposable) => { if (disposable) { disposable.dispose(); } }); }); - test('Ensure no activation commands are returned if the feature is disabled', async () => { - terminalSettings.setup(t => t.activateEnvironment).returns(() => false); - - const activationCommands = await terminalHelper.getEnvironmentActivationCommands(TerminalShellType.bash, undefined); - expect(activationCommands).to.equal(undefined, 'Activation commands should be undefined'); - }); - test('Conda activation for fish escapes spaces in conda filename', async () => { conda = 'path to conda'; const envName = 'EnvA'; const pythonPath = 'python3'; - platformService.setup(p => p.isWindows).returns(() => false); - condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve({ name: envName, path: path.dirname(pythonPath) })); + platformService.setup((p) => p.isWindows).returns(() => false); + componentAdapter + .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({ name: envName, path: path.dirname(pythonPath) })); const expected = ['"path to conda" activate EnvA']; - const provider = new CondaActivationCommandProvider(serviceContainer.object); + const provider = new CondaActivationCommandProvider( + condaService.object, + platformService.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 () => { + 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); + 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(serviceContainer.object); + componentAdapter + .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)); + const expected = [ + `source ${path.join(path.dirname(condaPath), 'activate').fileToCommandArgumentForPythonExt()} EnvA`, + ]; + + const provider = new CondaActivationCommandProvider( + condaService.object, + platformService.object, + configService.object, + componentAdapter.object, + ); const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.bash); expect(activationCommands).to.deep.equal(expected, 'Incorrect Activation command'); }); - test('Conda activation on bash uses "conda" after 4.4.0', async () => { - const envName = 'EnvA'; - const pythonPath = 'python3'; - const condaPath = path.join('a', 'b', 'c', 'conda'); - platformService.setup(p => p.isWindows).returns(() => false); - condaService.reset(); - condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())) - .returns(() => Promise.resolve({ - name: envName, - path: path.dirname(pythonPath) - })); - condaService.setup(c => c.getCondaFile()) - .returns(() => Promise.resolve(condaPath)); - condaService.setup(c => c.getCondaVersion()) - .returns(() => Promise.resolve(parse('4.4.0', true)!)); - const expected = [`source ${path.join(path.dirname(condaPath), 'activate').fileToCommandArgument()} EnvA`]; - - const provider = new CondaActivationCommandProvider(serviceContainer.object); - const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.bash); + const interpreterPath = path.join('path', 'to', 'interpreter'); + const environmentName = 'Env'; + const environmentNameHasSpaces = 'Env with spaces'; + 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, + }, + { + 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 Env'], + isWindows: false, + }, + { + testName: + 'Activation provides correct activation commands (windows) after 4.4.0 given interpreter path is provided, with spaces in env name', + envName: environmentNameHasSpaces, + expectedResult: ['path/to/activate', 'conda activate "Env with spaces"'], + isWindows: true, + }, + { + testName: + 'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, with spaces in env name', + envName: environmentNameHasSpaces, + expectedResult: ['source path/to/activate "Env with spaces"'], + isWindows: false, + }, + { + testName: + 'Activation provides correct activation commands (windows) after 4.4.0 given interpreter path is provided, and no env name', + envName: '', + expectedResult: ['path/to/activate', `conda activate .`], + isWindows: true, + }, + { + testName: + 'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, and no env name', + envName: '', + expectedResult: ['source path/to/activate .'], + 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, + }, + ]; - expect(activationCommands).to.deep.equal(expected, 'Incorrect Activation command'); + testsForActivationUsingInterpreterPath.forEach((testParams) => { + test(testParams.testName, async () => { + const pythonPath = 'python3'; + platformService.setup((p) => p.isWindows).returns(() => testParams.isWindows); + condaService.reset(); + componentAdapter + .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ + name: testParams.envName, + path: path.dirname(pythonPath), + }), + ); + 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, + componentAdapter.object, + ); + + const activationCommands = await provider.getActivationCommands( + undefined, + testParams.isWindows ? TerminalShellType.commandPrompt : TerminalShellType.bash, + ); + + expect(activationCommands).to.deep.equal(testParams.expectedResult, 'Incorrect Activation command'); + }); }); - async function expectNoCondaActivationCommandForPowershell(isWindows: boolean, isOsx: boolean, isLinux: boolean, pythonPath: string, shellType: TerminalShellType, hasSpaceInEnvironmentName = false) { - terminalSettings.setup(t => t.activateEnvironment).returns(() => true); - platformService.setup(p => p.isLinux).returns(() => isLinux); - platformService.setup(p => p.isWindows).returns(() => isWindows); - platformService.setup(p => p.isMac).returns(() => isOsx); - condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); - pythonSettings.setup(s => s.pythonPath).returns(() => pythonPath); - const envName = hasSpaceInEnvironmentName ? 'EnvA' : 'Env A'; - condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve({ name: envName, path: path.dirname(pythonPath) })); - - const activationCommands = await new CondaActivationCommandProvider(serviceContainer.object).getActivationCommands(undefined, shellType); - let expectedActivationCommamnd: string[] | undefined; + async function testCondaActivationCommands( + isWindows: boolean, + isOsx: boolean, + isLinux: boolean, + pythonPath: string, + shellType: TerminalShellType, + envName: string, + ) { + platformService.setup((p) => p.isLinux).returns(() => isLinux); + platformService.setup((p) => p.isWindows).returns(() => isWindows); + platformService.setup((p) => p.isMac).returns(() => isOsx); + componentAdapter.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + pythonSettings.setup((s) => s.pythonPath).returns(() => pythonPath); + 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, + componentAdapter.object, + ).getActivationCommands(undefined, shellType); + let expectedActivationCommand: string[] | undefined; + const expectEnvActivatePath = path.dirname(pythonPath); switch (shellType) { case TerminalShellType.powershell: - case TerminalShellType.powershellCore: { - expectedActivationCommamnd = undefined; - break; - } + case TerminalShellType.powershellCore: case TerminalShellType.fish: { - expectedActivationCommamnd = [`conda activate ${envName.toCommandArgument()}`]; + if (envName !== '') { + expectedActivationCommand = [`conda activate ${envName.toCommandArgumentForPythonExt()}`]; + } else { + expectedActivationCommand = [`conda activate ${expectEnvActivatePath}`]; + } break; } default: { - expectedActivationCommamnd = isWindows ? [`activate ${envName.toCommandArgument()}`] : [`source activate ${envName.toCommandArgument()}`]; + if (envName !== '') { + expectedActivationCommand = isWindows + ? [`activate ${envName.toCommandArgumentForPythonExt()}`] + : [`source activate ${envName.toCommandArgumentForPythonExt()}`]; + } else { + expectedActivationCommand = isWindows + ? [`activate ${expectEnvActivatePath}`] + : [`source activate ${expectEnvActivatePath}`]; + } break; } } - if (expectedActivationCommamnd) { - expect(activationCommands).to.deep.equal(expectedActivationCommamnd, 'Incorrect Activation command'); + if (expectedActivationCommand) { + expect(activationCommands).to.deep.equal(expectedActivationCommand, 'Incorrect Activation command'); } else { expect(activationCommands).to.equal(undefined, 'Incorrect Activation command'); } } - getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(shellType => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((shellType) => { test(`Conda activation command for shell ${shellType.name} on (windows)`, async () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); - await expectNoCondaActivationCommandForPowershell(true, false, false, pythonPath, shellType.value); + await testCondaActivationCommands(true, false, false, pythonPath, shellType.value, 'Env'); }); test(`Conda activation command for shell ${shellType.name} on (linux)`, async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - await expectNoCondaActivationCommandForPowershell(false, false, true, pythonPath, shellType.value); + await testCondaActivationCommands(false, false, true, pythonPath, shellType.value, 'Env'); }); test(`Conda activation command for shell ${shellType.name} on (mac)`, async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - await expectNoCondaActivationCommandForPowershell(false, true, false, pythonPath, shellType.value); + await testCondaActivationCommands(false, true, false, pythonPath, shellType.value, 'Env'); }); }); - getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(shellType => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((shellType) => { test(`Conda activation command for shell ${shellType.name} on (windows), containing spaces in environment name`, async () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); - await expectNoCondaActivationCommandForPowershell(true, false, false, pythonPath, shellType.value, true); + await testCondaActivationCommands(true, false, false, pythonPath, shellType.value, 'Env A'); }); test(`Conda activation command for shell ${shellType.name} on (linux), containing spaces in environment name`, async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - await expectNoCondaActivationCommandForPowershell(false, false, true, pythonPath, shellType.value, true); + await testCondaActivationCommands(false, false, true, pythonPath, shellType.value, 'Env A'); }); test(`Conda activation command for shell ${shellType.name} on (mac), containing spaces in environment name`, async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - await expectNoCondaActivationCommandForPowershell(false, true, false, pythonPath, shellType.value, true); + await testCondaActivationCommands(false, true, false, pythonPath, shellType.value, 'Env A'); }); }); - async function expectCondaActivationCommand(isWindows: boolean, isOsx: boolean, isLinux: boolean, pythonPath: string) { - terminalSettings.setup(t => t.activateEnvironment).returns(() => true); - platformService.setup(p => p.isLinux).returns(() => isLinux); - platformService.setup(p => p.isWindows).returns(() => isWindows); - platformService.setup(p => p.isMac).returns(() => isOsx); - condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); - pythonSettings.setup(s => s.pythonPath).returns(() => pythonPath); - condaService.setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve({ name: 'EnvA', path: path.dirname(pythonPath) })); + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((shellType) => { + test(`Conda activation command for shell ${shellType.name} on (windows), containing no environment name`, async () => { + const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); + await testCondaActivationCommands(true, false, false, pythonPath, shellType.value, ''); + }); + + test(`Conda activation command for shell ${shellType.name} on (linux), containing no environment name`, async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + await testCondaActivationCommands(false, false, true, pythonPath, shellType.value, ''); + }); + + test(`Conda activation command for shell ${shellType.name} on (mac), containing no environment name`, async () => { + const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); + await testCondaActivationCommands(false, true, false, pythonPath, shellType.value, ''); + }); + }); + async function expectCondaActivationCommand( + isWindows: boolean, + isOsx: boolean, + isLinux: boolean, + pythonPath: string, + ) { + platformService.setup((p) => p.isLinux).returns(() => isLinux); + platformService.setup((p) => p.isWindows).returns(() => isWindows); + platformService.setup((p) => p.isMac).returns(() => isOsx); + componentAdapter.setup((c) => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + pythonSettings.setup((s) => s.pythonPath).returns(() => pythonPath); + 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); + const activationCommands = await terminalHelper.getEnvironmentActivationCommands( + TerminalShellType.bash, + undefined, + ); expect(activationCommands).to.deep.equal(expectedActivationCommand, 'Incorrect Activation command'); } test('If environment is a conda environment, ensure conda activation command is sent (windows)', async () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))) + .returns(() => Promise.resolve(true)); await expectCondaActivationCommand(true, false, false, pythonPath); }); test('If environment is a conda environment, ensure conda activation command is sent (linux)', async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem + .setup((f) => + f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta'))), + ) + .returns(() => Promise.resolve(true)); await expectCondaActivationCommand(false, false, true, pythonPath); }); test('If environment is a conda environment, ensure conda activation command is sent (osx)', async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem + .setup((f) => + f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta'))), + ) + .returns(() => Promise.resolve(true)); await expectCondaActivationCommand(false, true, false, pythonPath); }); test('Get activation script command if environment is not a conda environment', async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - terminalSettings.setup(t => t.activateEnvironment).returns(() => true); - condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); - pythonSettings.setup(s => s.pythonPath).returns(() => pythonPath); + 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>(); - serviceContainer.setup(c => c.getAll(TypeMoq.It.isValue(ITerminalActivationCommandProvider), TypeMoq.It.isAny())).returns(() => [mockProvider.object]); - mockProvider.setup(p => p.isShellSupported(TypeMoq.It.isAny())).returns(() => true); - mockProvider.setup(p => p.getActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(['mock command'])); + serviceContainer + .setup((c) => c.getAll(TypeMoq.It.isValue(ITerminalActivationCommandProvider), TypeMoq.It.isAny())) + .returns(() => [mockProvider.object]); + mockProvider.setup((p) => p.isShellSupported(TypeMoq.It.isAny())).returns(() => true); + mockProvider + .setup((p) => p.getActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(['mock command'])); const expectedActivationCommand = ['mock command']; - const activationCommands = await terminalHelper.getEnvironmentActivationCommands(TerminalShellType.bash, undefined); + when(bash.isShellSupported(anything())).thenReturn(true); + when(bash.getActivationCommands(anything(), TerminalShellType.bash)).thenResolve(expectedActivationCommand); + + const activationCommands = await terminalHelper.getEnvironmentActivationCommands( + TerminalShellType.bash, + undefined, + ); + expect(activationCommands).to.deep.equal(expectedActivationCommand, 'Incorrect Activation command'); }); - async function expectActivationCommandIfCondaDetectionFails(isWindows: boolean, isOsx: boolean, isLinux: boolean, pythonPath: string, condaEnvsPath: string) { - terminalSettings.setup(t => t.activateEnvironment).returns(() => true); - platformService.setup(p => p.isLinux).returns(() => isLinux); - platformService.setup(p => p.isWindows).returns(() => isWindows); - platformService.setup(p => p.isMac).returns(() => isOsx); - condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); - condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); - pythonSettings.setup(s => s.pythonPath).returns(() => pythonPath); - - const mockProvider = TypeMoq.Mock.ofType<ITerminalActivationCommandProvider>(); - serviceContainer.setup(c => c.getAll(TypeMoq.It.isValue(ITerminalActivationCommandProvider), TypeMoq.It.isAny())).returns(() => [mockProvider.object]); - mockProvider.setup(p => p.isShellSupported(TypeMoq.It.isAny())).returns(() => true); - mockProvider.setup(p => p.getActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(['mock command'])); + async function expectActivationCommandIfCondaDetectionFails( + isWindows: boolean, + isOsx: boolean, + isLinux: boolean, + pythonPath: string, + ) { + platformService.setup((p) => p.isLinux).returns(() => isLinux); + platformService.setup((p) => p.isWindows).returns(() => isWindows); + platformService.setup((p) => p.isMac).returns(() => isOsx); + 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); + when(bash.getActivationCommands(anything(), TerminalShellType.bash)).thenResolve(['mock command']); const expectedActivationCommand = ['mock command']; - const activationCommands = await terminalHelper.getEnvironmentActivationCommands(TerminalShellType.bash, undefined); + const activationCommands = await terminalHelper.getEnvironmentActivationCommands( + TerminalShellType.bash, + undefined, + ); expect(activationCommands).to.deep.equal(expectedActivationCommand, 'Incorrect Activation command'); } test('If environment is a conda environment and environment detection fails, ensure activatino of script is sent (windows)', async () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); - 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 expectActivationCommandIfCondaDetectionFails(true, false, false, pythonPath, condaEnvDir); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))) + .returns(() => Promise.resolve(true)); + await expectActivationCommandIfCondaDetectionFails(true, false, false, pythonPath); }); test('If environment is a conda environment and environment detection fails, ensure activatino of script is sent (osx)', async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'python'); - const condaEnvDir = path.join('users', 'xyz', '.conda', 'envs'); - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); - await expectActivationCommandIfCondaDetectionFails(false, true, false, pythonPath, condaEnvDir); + fileSystem + .setup((f) => + f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta'))), + ) + .returns(() => Promise.resolve(true)); + await expectActivationCommandIfCondaDetectionFails(false, true, false, pythonPath); }); test('If environment is a conda environment and environment detection fails, ensure activatino of script is sent (linux)', async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'python'); - const condaEnvDir = path.join('users', 'xyz', '.conda', 'envs'); - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); - await expectActivationCommandIfCondaDetectionFails(false, false, true, pythonPath, condaEnvDir); + fileSystem + .setup((f) => + f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta'))), + ) + .returns(() => Promise.resolve(true)); + await expectActivationCommandIfCondaDetectionFails(false, false, true, pythonPath); }); test('Return undefined if unable to get activation command', async () => { const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); - terminalSettings.setup(t => t.activateEnvironment).returns(() => true); - 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); + pythonSettings.setup((s) => s.pythonPath).returns(() => pythonPath); const mockProvider = TypeMoq.Mock.ofType<ITerminalActivationCommandProvider>(); - serviceContainer.setup(c => c.getAll(TypeMoq.It.isValue(ITerminalActivationCommandProvider), TypeMoq.It.isAny())).returns(() => [mockProvider.object]); - mockProvider.setup(p => p.isShellSupported(TypeMoq.It.isAny())).returns(() => true); - mockProvider.setup(p => p.getActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); - - const activationCommands = await terminalHelper.getEnvironmentActivationCommands(TerminalShellType.bash, undefined); + serviceContainer + .setup((c) => c.getAll(TypeMoq.It.isValue(ITerminalActivationCommandProvider), TypeMoq.It.isAny())) + .returns(() => [mockProvider.object]); + mockProvider.setup((p) => p.isShellSupported(TypeMoq.It.isAny())).returns(() => true); + mockProvider + .setup((p) => p.getActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + + const activationCommands = await terminalHelper.getEnvironmentActivationCommands( + TerminalShellType.bash, + undefined, + ); expect(activationCommands).to.equal(undefined, 'Incorrect Activation command'); }); @@ -330,106 +577,103 @@ suite('Terminal Environment Activation conda', () => { terminalKind: TerminalShellType; }; - const testsForWindowsActivation: WindowsActivationTestParams[] = - [ - { - testName: 'Activation uses full path on windows for powershell', - basePath: windowsTestPath, - envName: 'TesterEnv', - expectedResult: undefined, - expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.powershell - }, - { - testName: 'Activation uses full path with spaces on windows for powershell', - basePath: windowsTestPathSpaces, - envName: 'TesterEnv', - expectedResult: undefined, - expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, - terminalKind: TerminalShellType.powershell - }, - { - testName: 'Activation uses full path on windows under powershell, environment name has spaces', - basePath: windowsTestPath, - envName: 'The Tester Environment', - expectedResult: undefined, - expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.powershell - }, - { - testName: 'Activation uses full path on windows for powershell-core', - basePath: windowsTestPath, - envName: 'TesterEnv', - expectedResult: undefined, - expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.powershellCore - }, - { - testName: 'Activation uses full path with spaces on windows for powershell-core', - basePath: windowsTestPathSpaces, - envName: 'TesterEnv', - expectedResult: undefined, - expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, - terminalKind: TerminalShellType.powershellCore - }, - { - testName: 'Activation uses full path on windows for powershell-core, environment name has spaces', - basePath: windowsTestPath, - envName: 'The Tester Environment', - expectedResult: undefined, - expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.powershellCore - }, - { - testName: 'Activation uses full path on windows for cmd.exe', - basePath: windowsTestPath, - envName: 'TesterEnv', - expectedResult: [`${path.join(windowsTestPath, 'activate')} TesterEnv`], - expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.commandPrompt - }, - { - testName: 'Activation uses full path with spaces on windows for cmd.exe', - basePath: windowsTestPathSpaces, - envName: 'TesterEnv', - expectedResult: [`"${path.join(windowsTestPathSpaces, 'activate')}" TesterEnv`], - expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, - terminalKind: TerminalShellType.commandPrompt - }, - { - testName: 'Activation uses full path on windows for cmd.exe, environment name has spaces', - basePath: windowsTestPath, - envName: 'The Tester Environment', - expectedResult: [`${path.join(windowsTestPath, 'activate')} "The Tester Environment"`], - expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.commandPrompt - } - ]; + const testsForWindowsActivation: WindowsActivationTestParams[] = [ + { + testName: 'Activation uses full path on windows for powershell', + basePath: windowsTestPath, + envName: 'TesterEnv', + expectedResult: ['conda activate TesterEnv'], + expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, + terminalKind: TerminalShellType.powershell, + }, + { + testName: 'Activation uses full path with spaces on windows for powershell', + basePath: windowsTestPathSpaces, + envName: 'TesterEnv', + expectedResult: ['conda activate TesterEnv'], + expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, + terminalKind: TerminalShellType.powershell, + }, + { + testName: 'Activation uses full path on windows under powershell, environment name has spaces', + basePath: windowsTestPath, + envName: 'The Tester Environment', + expectedResult: ['conda activate "The Tester Environment"'], + expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, + terminalKind: TerminalShellType.powershell, + }, + { + testName: 'Activation uses full path on windows for powershell-core', + basePath: windowsTestPath, + envName: 'TesterEnv', + expectedResult: ['conda activate TesterEnv'], + expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, + terminalKind: TerminalShellType.powershellCore, + }, + { + testName: 'Activation uses full path with spaces on windows for powershell-core', + basePath: windowsTestPathSpaces, + envName: 'TesterEnv', + expectedResult: ['conda activate TesterEnv'], + expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, + terminalKind: TerminalShellType.powershellCore, + }, + { + testName: 'Activation uses full path on windows for powershell-core, environment name has spaces', + basePath: windowsTestPath, + envName: 'The Tester Environment', + expectedResult: ['conda activate "The Tester Environment"'], + expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, + terminalKind: TerminalShellType.powershellCore, + }, + { + testName: 'Activation uses full path on windows for cmd.exe', + basePath: windowsTestPath, + envName: 'TesterEnv', + expectedResult: [`${path.join(windowsTestPath, 'activate')} TesterEnv`], + expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, + terminalKind: TerminalShellType.commandPrompt, + }, + { + testName: 'Activation uses full path with spaces on windows for cmd.exe', + basePath: windowsTestPathSpaces, + envName: 'TesterEnv', + expectedResult: [`"${path.join(windowsTestPathSpaces, 'activate')}" TesterEnv`], + expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, + terminalKind: TerminalShellType.commandPrompt, + }, + { + testName: 'Activation uses full path on windows for cmd.exe, environment name has spaces', + basePath: windowsTestPath, + envName: 'The Tester Environment', + expectedResult: [`${path.join(windowsTestPath, 'activate')} "The Tester Environment"`], + expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, + terminalKind: TerminalShellType.commandPrompt, + }, + ]; testsForWindowsActivation.forEach((testParams: WindowsActivationTestParams) => { test(testParams.testName, async () => { // each test simply tests the base windows activate command, // and then the specific result from the terminal selected. - const servCnt = TypeMoq.Mock.ofType<IServiceContainer>(); const condaSrv = TypeMoq.Mock.ofType<ICondaService>(); - condaSrv.setup(c => c.getCondaFile()) - .returns(async () => { - return path.join(testParams.basePath, 'conda.exe'); - }); - servCnt.setup(s => s.get(TypeMoq.It.isValue(ICondaService), TypeMoq.It.isAny())) - .returns(() => condaSrv.object); + condaSrv.setup((c) => c.getCondaFile()).returns(async () => path.join(testParams.basePath, 'conda.exe')); - const tstCmdProvider = new CondaActivationCommandProvider(servCnt.object); + const tstCmdProvider = new CondaActivationCommandProvider( + condaSrv.object, + platformService.object, + configService.object, + componentAdapter.object, + ); let result: string[] | undefined; if (testParams.terminalKind === TerminalShellType.commandPrompt) { result = await tstCmdProvider.getWindowsCommands(testParams.envName); } else { - result = await tstCmdProvider.getPowershellCommands(testParams.envName, testParams.terminalKind); + 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 91ad4dbe55f8..d87d33ea03e6 100644 --- a/src/test/common/terminals/activation.unit.test.ts +++ b/src/test/common/terminals/activation.unit.test.ts @@ -3,59 +3,174 @@ '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 } from 'vscode'; -import { ITerminalManager } from '../../../client/common/application/types'; -import { ITerminalActivator, ITerminalHelper } from '../../../client/common/terminal/types'; -import { IDisposableRegistry } from '../../../client/common/types'; -import { noop } from '../../../client/common/utils/misc'; -import { IServiceContainer } from '../../../client/ioc/types'; +import { Terminal, Uri } from 'vscode'; +import { ActiveResourceService } from '../../../client/common/application/activeResource'; +import { TerminalManager } from '../../../client/common/application/terminalManager'; +import { IActiveResourceService, ITerminalManager } from '../../../client/common/application/types'; +import { TerminalActivator } from '../../../client/common/terminal/activator'; +import { ITerminalActivator } from '../../../client/common/terminal/types'; +import { IDisposable } from '../../../client/common/types'; import { TerminalAutoActivation } from '../../../client/terminals/activation'; import { ITerminalAutoActivation } from '../../../client/terminals/types'; +import { noop } from '../../core'; +import * as extapi from '../../../client/envExt/api.internal'; suite('Terminal Auto Activation', () => { - let activator: TypeMoq.IMock<ITerminalActivator>; - let terminalManager: TypeMoq.IMock<ITerminalManager>; + let activator: ITerminalActivator; + let terminalManager: ITerminalManager; let terminalAutoActivation: ITerminalAutoActivation; + let activeResourceService: IActiveResourceService; + const resource = Uri.parse('a'); + let terminal: Terminal; setup(() => { - terminalManager = TypeMoq.Mock.ofType<ITerminalManager>(); - activator = TypeMoq.Mock.ofType<ITerminalActivator>(); - const disposables = []; - - const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(ITerminalManager), TypeMoq.It.isAny())) - .returns(() => terminalManager.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(ITerminalHelper), TypeMoq.It.isAny())) - .returns(() => activator.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) - .returns(() => disposables); - - terminalAutoActivation = new TerminalAutoActivation(serviceContainer.object, activator.object); + sinon.stub(extapi, 'shouldEnvExtHandleActivation').returns(false); + terminal = ({ + dispose: noop, + hide: noop, + name: 'Python', + creationOptions: {}, + processId: Promise.resolve(0), + sendText: noop, + show: noop, + exitStatus: { code: 0 }, + } as unknown) as Terminal; + terminalManager = mock(TerminalManager); + activator = mock(TerminalActivator); + activeResourceService = mock(ActiveResourceService); + + terminalAutoActivation = new TerminalAutoActivation( + instance(terminalManager), + [], + instance(activator), + instance(activeResourceService), + ); + }); + teardown(() => { + sinon.restore(); }); test('New Terminals should be activated', async () => { - let eventHandler: undefined | ((e: Terminal) => void); - const terminal = TypeMoq.Mock.ofType<Terminal>(); - terminalManager - .setup(m => m.onDidOpenTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(handler => { - eventHandler = handler; - return { dispose: noop }; - }); - activator - .setup(h => h.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .verifiable(TypeMoq.Times.once()); + type EventHandler = (e: Terminal) => void; + let handler: undefined | EventHandler; + const handlerDisposable = TypeMoq.Mock.ofType<IDisposable>(); + const onDidOpenTerminal = (cb: EventHandler) => { + handler = cb; + return handlerDisposable.object; + }; + when(activeResourceService.getActiveResource()).thenReturn(resource); + when(terminalManager.onDidOpenTerminal).thenReturn(onDidOpenTerminal); + when(activator.activateEnvironmentInTerminal(anything(), anything())).thenResolve(); + + terminalAutoActivation.register(); + expect(handler).not.to.be.an('undefined', 'event handler not initialized'); + + handler!.bind(terminalAutoActivation)(terminal); + + verify(activator.activateEnvironmentInTerminal(terminal, anything())).once(); + }); + test('New Terminals should not be activated if hidden from user', async () => { + terminal = ({ + dispose: noop, + hide: noop, + name: 'Python', + creationOptions: { hideFromUser: true }, + processId: Promise.resolve(0), + sendText: noop, + show: noop, + exitStatus: { code: 0 }, + } 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>(); + 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(); + terminalAutoActivation.disableAutoActivation(terminal); + + expect(handler).not.to.be.an('undefined', 'event handler not initialized'); + + handler!.bind(terminalAutoActivation)(terminal); + + verify(activator.activateEnvironmentInTerminal(terminal, anything())).never(); + }); + test('New Terminals should be activated with resource of single workspace', async () => { + type EventHandler = (e: Terminal) => void; + let handler: undefined | EventHandler; + const handlerDisposable = TypeMoq.Mock.ofType<IDisposable>(); + const onDidOpenTerminal = (cb: EventHandler) => { + handler = cb; + return handlerDisposable.object; + }; + when(activeResourceService.getActiveResource()).thenReturn(resource); + when(terminalManager.onDidOpenTerminal).thenReturn(onDidOpenTerminal); + when(activator.activateEnvironmentInTerminal(anything(), anything())).thenResolve(); + + terminalAutoActivation.register(); + + expect(handler).not.to.be.an('undefined', 'event handler not initialized'); + + handler!.bind(terminalAutoActivation)(terminal); + + verify(activator.activateEnvironmentInTerminal(terminal, anything())).once(); + }); + test('New Terminals should be activated with resource of main workspace', async () => { + type EventHandler = (e: Terminal) => void; + let handler: undefined | EventHandler; + const handlerDisposable = TypeMoq.Mock.ofType<IDisposable>(); + const onDidOpenTerminal = (cb: EventHandler) => { + handler = cb; + return handlerDisposable.object; + }; + when(activeResourceService.getActiveResource()).thenReturn(resource); + when(terminalManager.onDidOpenTerminal).thenReturn(onDidOpenTerminal); + when(activator.activateEnvironmentInTerminal(anything(), anything())).thenResolve(); terminalAutoActivation.register(); - expect(eventHandler).not.to.be.an('undefined', 'event handler not initialized'); + expect(handler).not.to.be.an('undefined', 'event handler not initialized'); - eventHandler!.bind(terminalAutoActivation)(terminal.object); + handler!.bind(terminalAutoActivation)(terminal); - activator.verifyAll(); + verify(activator.activateEnvironmentInTerminal(terminal, anything())).once(); }); }); diff --git a/src/test/common/terminals/activator/base.unit.test.ts b/src/test/common/terminals/activator/base.unit.test.ts index 48c37c363319..fdfe9dcee579 100644 --- a/src/test/common/terminals/activator/base.unit.test.ts +++ b/src/test/common/terminals/activator/base.unit.test.ts @@ -3,94 +3,105 @@ 'use strict'; -import * as TypeMoq from 'typemoq'; import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; import { Terminal } from 'vscode'; import { BaseTerminalActivator } from '../../../../client/common/terminal/activator/base'; import { ITerminalActivator, ITerminalHelper } from '../../../../client/common/terminal/types'; import { noop } from '../../../../client/common/utils/misc'; -// tslint:disable:max-func-body-length no-any suite('Terminal Base Activator', () => { let activator: ITerminalActivator; let helper: TypeMoq.IMock<ITerminalHelper>; setup(() => { helper = TypeMoq.Mock.ofType<ITerminalHelper>(); - activator = new class extends BaseTerminalActivator { - public waitForCommandToProcess() { noop(); return Promise.resolve(); } - }(helper.object) as any as ITerminalActivator; + activator = (new (class extends BaseTerminalActivator { + public waitForCommandToProcess() { + noop(); + return Promise.resolve(); + } + })(helper.object) as any) as ITerminalActivator; }); [ { commandCount: 1, preserveFocus: false }, { commandCount: 2, preserveFocus: false }, { commandCount: 1, preserveFocus: true }, - { commandCount: 1, preserveFocus: true } - ].forEach(item => { + { 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.getTerminalShellPath()).returns(() => ''); - helper.setup(h => h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(activationCommands)); + helper + .setup((h) => + h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + ) + .returns(() => Promise.resolve(activationCommands)); const terminal = TypeMoq.Mock.ofType<Terminal>(); terminal - .setup(t => t.show(TypeMoq.It.isValue(item.preserveFocus))) + .setup((t) => t.show(TypeMoq.It.isValue(item.preserveFocus))) .returns(() => undefined) .verifiable(TypeMoq.Times.exactly(activationCommands.length)); - activationCommands.forEach(cmd => { + activationCommands.forEach((cmd) => { terminal - .setup(t => t.sendText(TypeMoq.It.isValue(cmd))) + .setup((t) => t.sendText(TypeMoq.It.isValue(cmd))) .returns(() => undefined) .verifiable(TypeMoq.Times.exactly(1)); }); - await activator.activateEnvironmentInTerminal(terminal.object, undefined, item.preserveFocus); + await activator.activateEnvironmentInTerminal(terminal.object, { preserveFocus: item.preserveFocus }); terminal.verifyAll(); }); test(`Terminal is activated only once ${titleSuffix}`, async () => { - helper.setup(h => h.getTerminalShellPath()).returns(() => ''); - helper.setup(h => h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(activationCommands)); + helper + .setup((h) => + h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + ) + .returns(() => Promise.resolve(activationCommands)); const terminal = TypeMoq.Mock.ofType<Terminal>(); terminal - .setup(t => t.show(TypeMoq.It.isValue(item.preserveFocus))) + .setup((t) => t.show(TypeMoq.It.isValue(item.preserveFocus))) .returns(() => undefined) .verifiable(TypeMoq.Times.exactly(activationCommands.length)); - activationCommands.forEach(cmd => { + activationCommands.forEach((cmd) => { terminal - .setup(t => t.sendText(TypeMoq.It.isValue(cmd))) + .setup((t) => t.sendText(TypeMoq.It.isValue(cmd))) .returns(() => undefined) .verifiable(TypeMoq.Times.exactly(1)); }); - await activator.activateEnvironmentInTerminal(terminal.object, undefined, item.preserveFocus); - await activator.activateEnvironmentInTerminal(terminal.object, undefined, item.preserveFocus); - await activator.activateEnvironmentInTerminal(terminal.object, undefined, item.preserveFocus); + await activator.activateEnvironmentInTerminal(terminal.object, { preserveFocus: item.preserveFocus }); + await activator.activateEnvironmentInTerminal(terminal.object, { preserveFocus: item.preserveFocus }); + await activator.activateEnvironmentInTerminal(terminal.object, { preserveFocus: item.preserveFocus }); terminal.verifyAll(); }); test(`Terminal is activated only once ${titleSuffix} (even when not waiting)`, async () => { - helper.setup(h => h.getTerminalShellPath()).returns(() => ''); - helper.setup(h => h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(activationCommands)); + helper + .setup((h) => + h.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + ) + .returns(() => Promise.resolve(activationCommands)); const terminal = TypeMoq.Mock.ofType<Terminal>(); terminal - .setup(t => t.show(TypeMoq.It.isValue(item.preserveFocus))) + .setup((t) => t.show(TypeMoq.It.isValue(item.preserveFocus))) .returns(() => undefined) .verifiable(TypeMoq.Times.exactly(activationCommands.length)); - activationCommands.forEach(cmd => { + activationCommands.forEach((cmd) => { terminal - .setup(t => t.sendText(TypeMoq.It.isValue(cmd))) + .setup((t) => t.sendText(TypeMoq.It.isValue(cmd))) .returns(() => undefined) .verifiable(TypeMoq.Times.exactly(1)); }); const activated = await Promise.all([ - activator.activateEnvironmentInTerminal(terminal.object, undefined, item.preserveFocus), - activator.activateEnvironmentInTerminal(terminal.object, undefined, item.preserveFocus), - activator.activateEnvironmentInTerminal(terminal.object, undefined, 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 91d1766b1a74..34d1cf8f1bcd 100644 --- a/src/test/common/terminals/activator/index.unit.test.ts +++ b/src/test/common/terminals/activator/index.unit.test.ts @@ -3,47 +3,207 @@ '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 } from '../../../../client/common/terminal/types'; +import { + ITerminalActivationHandler, + ITerminalActivator, + ITerminalHelper, +} from '../../../../client/common/terminal/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>(); - activator = new class extends TerminalActivator { + const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + configService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns(() => { + return ({ + terminal: terminalSettings.object, + } as unknown) as IPythonSettings; + }); + activator = new (class extends TerminalActivator { protected initialize() { this.baseActivator = baseActivator.object; } - }(TypeMoq.Mock.ofType<ITerminalHelper>().object, [handler1.object, handler2.object]); + })( + TypeMoq.Mock.ofType<ITerminalHelper>().object, + [handler1.object, handler2.object], + configService.object, + experimentService.object, + ); + }); + teardown(() => { + sinon.restore(); }); - async function testActivationAndHandlers(activationSuccessful: boolean) { + + async function testActivationAndHandlers( + activationSuccessful: boolean, + activateEnvironmentSetting: boolean, + hidden: boolean = false, + ) { + terminalSettings + .setup((b) => b.activateEnvironment) + .returns(() => activateEnvironmentSetting) + .verifiable(TypeMoq.Times.once()); baseActivator - .setup(b => b.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .setup((b) => b.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(activationSuccessful)) - .verifiable(TypeMoq.Times.once()); - handler1.setup(h => h.handleActivation(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isValue(activationSuccessful))) + .verifiable(TypeMoq.Times.exactly(activationSuccessful ? 1 : 0)); + handler1 + .setup((h) => + h.handleActivation( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isValue(activationSuccessful), + ), + ) .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - handler2.setup(h => h.handleActivation(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isValue(activationSuccessful))) + .verifiable(TypeMoq.Times.exactly(activationSuccessful ? 1 : 0)); + handler2 + .setup((h) => + h.handleActivation( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isValue(activationSuccessful), + ), + ) .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); + .verifiable(TypeMoq.Times.exactly(activationSuccessful ? 1 : 0)); const terminal = TypeMoq.Mock.ofType<Terminal>(); - await activator.activateEnvironmentInTerminal(terminal.object, undefined, activationSuccessful); + const activated = await activator.activateEnvironmentInTerminal(terminal.object, { + preserveFocus: activationSuccessful, + hideFromUser: hidden, + }); + assert.strictEqual(activated, activationSuccessful); baseActivator.verifyAll(); handler1.verifyAll(); handler2.verifyAll(); } - test('Terminal is activated and handlers are invoked', () => testActivationAndHandlers(true)); - test('Terminal is not activated and handlers are invoked', () => testActivationAndHandlers(false)); + test('Terminal is activated and handlers are invoked', () => testActivationAndHandlers(true, true)); + test('Terminal is not activated if auto-activate setting is set to true but terminal is hidden', () => + testActivationAndHandlers(false, true, true)); + test('Terminal is not activated and handlers are invoked', () => testActivationAndHandlers(false, false)); + + 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 1711af2988f4..9bf1afbbad03 100644 --- a/src/test/common/terminals/activator/powerShellFailedHandler.unit.test.ts +++ b/src/test/common/terminals/activator/powerShellFailedHandler.unit.test.ts @@ -8,63 +8,101 @@ import { Terminal } from 'vscode'; import { IDiagnosticsService } from '../../../../client/application/diagnostics/types'; import { IPlatformService } from '../../../../client/common/platform/types'; import { PowershellTerminalActivationFailedHandler } from '../../../../client/common/terminal/activator/powershellFailedHandler'; -import { ITerminalActivationHandler, ITerminalHelper, TerminalShellType } from '../../../../client/common/terminal/types'; +import { + ITerminalActivationHandler, + ITerminalHelper, + TerminalShellType, +} from '../../../../client/common/terminal/types'; import { getNamesAndValues } from '../../../../client/common/utils/enum'; -// tslint:disable-next-line:max-func-body-length suite('Terminal Activation Powershell Failed Handler', () => { let psHandler: ITerminalActivationHandler; let helper: TypeMoq.IMock<ITerminalHelper>; let platform: TypeMoq.IMock<IPlatformService>; let diagnosticService: TypeMoq.IMock<IDiagnosticsService>; - async function testDiagnostics(mustHandleDiagnostics: boolean, isWindows: boolean, activatedSuccessfully: boolean, shellType: TerminalShellType, cmdPromptHasActivationCommands: boolean) { - platform.setup(p => p.isWindows).returns(() => isWindows); - helper - .setup(p => p.getTerminalShellPath()) - .returns(() => ''); - // .verifiable(TypeMoq.Times.atMostOnce()); - helper - .setup(p => p.identifyTerminalShell(TypeMoq.It.isAny())) - .returns(() => shellType); - // .verifiable(TypeMoq.Times.atMostOnce());c + async function testDiagnostics( + mustHandleDiagnostics: boolean, + isWindows: boolean, + activatedSuccessfully: boolean, + shellType: TerminalShellType, + cmdPromptHasActivationCommands: boolean, + ) { + platform.setup((p) => p.isWindows).returns(() => isWindows); + helper.setup((p) => p.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => shellType); const cmdPromptCommands = cmdPromptHasActivationCommands ? ['a'] : []; - helper.setup(h => h.getEnvironmentActivationCommands(TypeMoq.It.isValue(TerminalShellType.commandPrompt), TypeMoq.It.isAny())) + helper + .setup((h) => + h.getEnvironmentActivationCommands( + TypeMoq.It.isValue(TerminalShellType.commandPrompt), + TypeMoq.It.isAny(), + ), + ) .returns(() => Promise.resolve(cmdPromptCommands)); - // .verifiable(TypeMoq.Times.atMostOnce()); diagnosticService - .setup(d => d.handle(TypeMoq.It.isAny())) + .setup((d) => d.handle(TypeMoq.It.isAny())) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.exactly(mustHandleDiagnostics ? 1 : 0)); - await psHandler.handleActivation(TypeMoq.Mock.ofType<Terminal>().object, undefined, false, activatedSuccessfully); + await psHandler.handleActivation( + TypeMoq.Mock.ofType<Terminal>().object, + undefined, + false, + activatedSuccessfully, + ); } - [true, false].forEach(isWindows => { + [true, false].forEach((isWindows) => { suite(`OS is ${isWindows ? 'Windows' : 'Non-Widows'}`, () => { - getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(shell => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((shell) => { suite(`Shell is ${shell.name}`, () => { - [true, false].forEach(hasCommandPromptActivations => { - hasCommandPromptActivations = isWindows && hasCommandPromptActivations && shell.value !== TerminalShellType.commandPrompt; - suite(`${hasCommandPromptActivations ? 'Can activate with Command Prompt' : 'Can\'t activate with Command Prompt'}`, () => { - [true, false].forEach(activatedSuccessfully => { - suite(`Terminal Activation is ${activatedSuccessfully ? 'successful' : 'has failed'}`, () => { - setup(() => { - helper = TypeMoq.Mock.ofType<ITerminalHelper>(); - platform = TypeMoq.Mock.ofType<IPlatformService>(); - diagnosticService = TypeMoq.Mock.ofType<IDiagnosticsService>(); - psHandler = new PowershellTerminalActivationFailedHandler(helper.object, platform.object, diagnosticService.object); - }); - const isPs = shell.value === TerminalShellType.powershell || shell.value === TerminalShellType.powershellCore; - const mustHandleDiagnostics = isPs && !activatedSuccessfully && hasCommandPromptActivations; - test(`Diagnostic must ${mustHandleDiagnostics ? 'be' : 'not be'} handled`, async () => { - await testDiagnostics(mustHandleDiagnostics, isWindows, activatedSuccessfully, shell.value, hasCommandPromptActivations); - helper.verifyAll(); - diagnosticService.verifyAll(); - }); + [true, false].forEach((hasCommandPromptActivations) => { + hasCommandPromptActivations = + isWindows && hasCommandPromptActivations && shell.value !== TerminalShellType.commandPrompt; + suite( + `${ + hasCommandPromptActivations + ? 'Can activate with Command Prompt' + : "Can't activate with Command Prompt" + }`, + () => { + [true, false].forEach((activatedSuccessfully) => { + suite( + `Terminal Activation is ${activatedSuccessfully ? 'successful' : 'has failed'}`, + () => { + setup(() => { + helper = TypeMoq.Mock.ofType<ITerminalHelper>(); + platform = TypeMoq.Mock.ofType<IPlatformService>(); + diagnosticService = TypeMoq.Mock.ofType<IDiagnosticsService>(); + psHandler = new PowershellTerminalActivationFailedHandler( + helper.object, + platform.object, + diagnosticService.object, + ); + }); + const isPs = + shell.value === TerminalShellType.powershell || + shell.value === TerminalShellType.powershellCore; + const mustHandleDiagnostics = + isPs && !activatedSuccessfully && hasCommandPromptActivations; + test(`Diagnostic must ${ + mustHandleDiagnostics ? 'be' : 'not be' + } handled`, async () => { + await testDiagnostics( + mustHandleDiagnostics, + isWindows, + activatedSuccessfully, + shell.value, + hasCommandPromptActivations, + ); + helper.verifyAll(); + diagnosticService.verifyAll(); + }); + }, + ); }); - }); - }); + }, + ); }); }); }); diff --git a/src/test/common/terminals/commandPrompt.unit.test.ts b/src/test/common/terminals/commandPrompt.unit.test.ts index 9c99fcf4759c..acea3f5f35a9 100644 --- a/src/test/common/terminals/commandPrompt.unit.test.ts +++ b/src/test/common/terminals/commandPrompt.unit.test.ts @@ -7,7 +7,10 @@ import { expect } from 'chai'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; import { ConfigurationTarget } from 'vscode'; -import { getCommandPromptLocation, useCommandPromptAsDefaultShell } from '../../../client/common/terminal/commandPrompt'; +import { + getCommandPromptLocation, + useCommandPromptAsDefaultShell, +} from '../../../client/common/terminal/commandPrompt'; import { IConfigurationService, ICurrentProcess } from '../../../client/common/types'; suite('Terminal Command Prompt', () => { @@ -21,7 +24,8 @@ suite('Terminal Command Prompt', () => { test('Getting Path Command Prompt executable (32 on 64Win)', async () => { const env = { windir: 'windir' }; - currentProc.setup(p => p.env) + currentProc + .setup((p) => p.env) .returns(() => env) .verifiable(TypeMoq.Times.atLeastOnce()); @@ -32,7 +36,8 @@ suite('Terminal Command Prompt', () => { }); test('Getting Path Command Prompt executable (not 32 on 64Win)', async () => { const env = { PROCESSOR_ARCHITEW6432: 'x', windir: 'windir' }; - currentProc.setup(p => p.env) + currentProc + .setup((p) => p.env) .returns(() => env) .verifiable(TypeMoq.Times.atLeastOnce()); @@ -43,14 +48,21 @@ suite('Terminal Command Prompt', () => { }); test('Use command prompt as default shell', async () => { const env = { windir: 'windir' }; - currentProc.setup(p => p.env) + currentProc + .setup((p) => p.env) .returns(() => env) .verifiable(TypeMoq.Times.atLeastOnce()); const cmdPromptPath = path.join('windir', 'System32', 'cmd.exe'); configService - .setup(c => c.updateSectionSetting(TypeMoq.It.isValue('terminal'), TypeMoq.It.isValue('integrated.shell.windows'), - TypeMoq.It.isValue(cmdPromptPath), TypeMoq.It.isAny(), - TypeMoq.It.isValue(ConfigurationTarget.Global))) + .setup((c) => + c.updateSectionSetting( + TypeMoq.It.isValue('terminal'), + TypeMoq.It.isValue('integrated.shell.windows'), + TypeMoq.It.isValue(cmdPromptPath), + TypeMoq.It.isAny(), + TypeMoq.It.isValue(ConfigurationTarget.Global), + ), + ) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); diff --git a/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts b/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts new file mode 100644 index 000000000000..5d963b8aa2c2 --- /dev/null +++ b/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { instance, mock, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { 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 } from '../../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../../client/interpreter/interpreterService'; +import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; + +suite('Terminals Activation - Pipenv', () => { + [undefined, Uri.parse('x')].forEach((resource) => { + suite(resource ? 'With a resource' : 'Without a resource', () => { + let pipenvExecFile = 'pipenv'; + let activationProvider: ITerminalActivationCommandProvider; + let interpreterService: IInterpreterService; + let pipEnvExecution: TypeMoq.IMock<IToolExecutionPath>; + let workspaceService: IWorkspaceService; + setup(() => { + interpreterService = mock(InterpreterService); + workspaceService = mock(WorkspaceService); + interpreterService = mock(InterpreterService); + pipEnvExecution = TypeMoq.Mock.ofType<IToolExecutionPath>(); + activationProvider = new PipEnvActivationCommandProvider( + instance(interpreterService), + pipEnvExecution.object, + instance(workspaceService), + ); + + pipEnvExecution.setup((p) => p.executable).returns(() => pipenvExecFile); + }); + + test('No commands for no interpreter', async () => { + when(interpreterService.getActiveInterpreter(resource)).thenResolve(); + + for (const shell of getNamesAndValues<TerminalShellType>(TerminalShellType)) { + const cmd = await activationProvider.getActivationCommands(resource, shell.value); + + assert.strictEqual(cmd, undefined); + } + }); + test('No commands for an interpreter that is not Pipenv', async () => { + const nonPipInterpreterTypes = getNamesAndValues<EnvironmentType>(EnvironmentType).filter( + (t) => t.value !== EnvironmentType.Pipenv, + ); + for (const interpreterType of nonPipInterpreterTypes) { + when(interpreterService.getActiveInterpreter(resource)).thenResolve({ + type: interpreterType, + // 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.strictEqual(cmd, undefined); + } + } + }); + test('pipenv shell is returned for pipenv interpeter', async () => { + when(interpreterService.getActiveInterpreter(resource)).thenResolve({ + envType: EnvironmentType.Pipenv, + // 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.deepEqual(cmd, ['pipenv shell']); + } + }); + test('pipenv is properly escaped', async () => { + pipenvExecFile = 'my pipenv'; + when(interpreterService.getActiveInterpreter(resource)).thenResolve({ + envType: EnvironmentType.Pipenv, + // 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.deepEqual(cmd, ['"my pipenv" shell']); + } + }); + }); + }); +}); diff --git a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts index 0afd5c99b564..5a5e65a9c0f2 100644 --- a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts +++ b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts @@ -4,21 +4,39 @@ '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 { FileSystem } from '../../../../client/common/platform/fileSystem'; import { PYTHON_VIRTUAL_ENVS_LOCATION } from '../../../ciConstants'; -import { PYTHON_PATH, restorePythonPathInWorkspaceRoot, setPythonPathInWorkspaceRoot, updateSetting, waitForCondition } from '../../../common'; -import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { + PYTHON_PATH, + restorePythonPathInWorkspaceRoot, + setPythonPathInWorkspaceRoot, + updateSetting, + 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-next-line:max-func-body-length suite('Activation of Environments in Terminal', () => { - const file = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', 'testExecInTerminal.py'); - const outputFile = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', 'testExecInTerminal.log'); - const envsLocation = PYTHON_VIRTUAL_ENVS_LOCATION !== undefined ? - path.join(EXTENSION_ROOT_DIR_FOR_TESTS, PYTHON_VIRTUAL_ENVS_LOCATION) : path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'tmp', 'envPaths.json'); + const file = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'testExecInTerminal.py', + ); + let outputFile = ''; + let outputFileCounter = 0; + const fileSystem = new FileSystem(); + const outputFilesCreated: string[] = []; + const envsLocation = + PYTHON_VIRTUAL_ENVS_LOCATION !== undefined + ? path.join(EXTENSION_ROOT_DIR_FOR_TESTS, PYTHON_VIRTUAL_ENVS_LOCATION) + : path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'tmp', 'envPaths.json'); const waitTimeForActivation = 5000; type EnvPath = { condaExecPath: string; @@ -31,71 +49,148 @@ suite('Activation of Environments in Terminal', () => { const defaultShell = { Windows: '', Linux: '', - MacOS: '' + MacOS: '', }; - let terminalSettings; - let pythonSettings; + let terminalSettings: any; + let pythonSettings: any; + 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); - await initialize(); + 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.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(); - await cleanUp(); + outputFile = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + `testExecInTerminal_${outputFileCounter}.log`, + ); + outputFileCounter += 1; + outputFilesCreated.push(outputFile); }); - teardown(cleanUp); - suiteTeardown(revertSettings); + + suiteTeardown(async function () { + sandbox.restore(); + this.timeout(TEST_TIMEOUT * 2); + await revertSettings(); + + // remove all created log files. + outputFilesCreated.forEach(async (filePath: string) => { + if (await fs.pathExists(filePath)) { + await fs.unlink(filePath); + } + }); + }); + async function revertSettings() { - await updateSetting('terminal.activateEnvironment', undefined, vscode.workspace.workspaceFolders[0].uri, vscode.ConfigurationTarget.WorkspaceFolder); - await terminalSettings.update('integrated.shell.windows', defaultShell.Windows, vscode.ConfigurationTarget.Global); - await terminalSettings.update('integrated.shell.linux', defaultShell.Linux, vscode.ConfigurationTarget.Global); - await pythonSettings.update('condaPath', undefined, vscode.ConfigurationTarget.Workspace); + await updateSetting( + 'terminal.activateEnvironment', + undefined, + vscode.workspace.workspaceFolders![0].uri, + vscode.ConfigurationTarget.WorkspaceFolder, + ); + await terminalSettings.update( + 'integrated.defaultProfile.windows', + defaultShell.Windows, + vscode.ConfigurationTarget.Global, + ); + await terminalSettings.update( + 'integrated.defaultProfile.linux', + defaultShell.Linux, + vscode.ConfigurationTarget.Global, + ); + await pythonSettings.update('condaPath', undefined, vscode.ConfigurationTarget.Global); await restorePythonPathInWorkspaceRoot(); } - async function cleanUp() { - if (await fs.pathExists(outputFile)) { - await fs.unlink(outputFile); - } - } - async function testActivation(envPath) { - await updateSetting('terminal.activateEnvironment', true, vscode.workspace.workspaceFolders[0].uri, vscode.ConfigurationTarget.WorkspaceFolder); - await setPythonPathInWorkspaceRoot(envPath); + /** + * Open a terminal and issue a python `pythonFile` command, expecting it to + * create a file `logfile`, with timeout limits. + * + * @param pythonFile The python script to run. + * @param logFile The logfile that the python script will produce. + * @param consoleInitWaitMs How long to wait for the console to initialize. + * @param logFileCreationWaitMs How long to wait for the output file to be produced. + */ + async function openTerminalAndAwaitCommandContent( + consoleInitWaitMs: number, + pythonFile: string, + logFile: string, + logFileCreationWaitMs: number, + ): Promise<string> { const terminal = vscode.window.createTerminal(); - await sleep(waitTimeForActivation); - terminal.sendText(`python ${file}`, true); - await waitForCondition(() => fs.pathExists(outputFile), 5_000, '\'testExecInTerminal.log\' file not created'); - const content = await fs.readFile(outputFile, 'utf-8'); - expect(content).to.equal(envPath); + await sleep(consoleInitWaitMs); + terminal.sendText( + `python ${pythonFile.toCommandArgumentForPythonExt()} ${logFile.toCommandArgumentForPythonExt()}`, + true, + ); + await waitForCondition(() => fs.pathExists(logFile), logFileCreationWaitMs, `${logFile} file not created.`); + + return fs.readFile(logFile, 'utf-8'); } - async function testNonActivation() { - await updateSetting('terminal.activateEnvironment', false, vscode.workspace.workspaceFolders[0].uri, vscode.ConfigurationTarget.WorkspaceFolder); - const terminal = vscode.window.createTerminal(); - terminal.sendText(`python ${file}`, true); - await waitForCondition(() => fs.pathExists(outputFile), 5_000, '\'testExecInTerminal.log\' file not created'); - const content = await fs.readFile(outputFile, 'utf-8'); - expect(content).to.not.equal(PYTHON_PATH); + + /** + * Turn on `terminal.activateEnvironment`, produce a shell, run a python script + * that outputs the path to the active python interpreter. + * + * Note: asserts that the envPath given matches the envPath returned by the script. + * + * @param envPath Python environment path to activate in the terminal (via vscode config) + */ + async function testActivation(envPath: string) { + await updateSetting( + 'terminal.activateEnvironment', + true, + vscode.workspace.workspaceFolders![0].uri, + vscode.ConfigurationTarget.WorkspaceFolder, + ); + await setPythonPathInWorkspaceRoot(envPath); + const content = await openTerminalAndAwaitCommandContent(waitTimeForActivation, file, outputFile, 5_000); + expect(fileSystem.arePathsSame(content, envPath)).to.equal(true, 'Environment not activated'); } + test('Should not activate', async () => { - await testNonActivation(); + await updateSetting( + 'terminal.activateEnvironment', + false, + vscode.workspace.workspaceFolders![0].uri, + vscode.ConfigurationTarget.WorkspaceFolder, + ); + const content = await openTerminalAndAwaitCommandContent(waitTimeForActivation, file, outputFile, 5_000); + expect(fileSystem.arePathsSame(content, PYTHON_PATH)).to.equal(false, 'Environment not activated'); }); - test('Should activate with venv', async () => { + + test('Should activate with venv', async function () { + if (process.env.CI_PYTHON_VERSION && process.env.CI_PYTHON_VERSION.startsWith('2.')) { + 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 () => { - await terminalSettings.update('integrated.shell.windows', 'C:\\Windows\\System32\\cmd.exe', vscode.ConfigurationTarget.Global); - await pythonSettings.update('condaPath', envPaths.condaExecPath, vscode.ConfigurationTarget.Workspace); + test('Should activate with conda', async function () { + // Powershell does not work with conda by default, hence use cmd. + await terminalSettings.update( + 'integrated.defaultProfile.windows', + 'Command Prompt', + vscode.ConfigurationTarget.Global, + ); + 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 232beb41246b..5ad2da8e793a 100644 --- a/src/test/common/terminals/factory.unit.test.ts +++ b/src/test/common/terminals/factory.unit.test.ts @@ -5,36 +5,48 @@ import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; import { Disposable, Uri, WorkspaceFolder } from 'vscode'; import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { IFileSystem } from '../../../client/common/platform/types'; import { TerminalServiceFactory } from '../../../client/common/terminal/factory'; import { TerminalService } from '../../../client/common/terminal/service'; +import { SynchronousTerminalService } from '../../../client/common/terminal/syncTerminalService'; import { ITerminalHelper, ITerminalServiceFactory } from '../../../client/common/terminal/types'; import { IDisposableRegistry } from '../../../client/common/types'; import { IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; -// tslint:disable-next-line:max-func-body-length suite('Terminal Service Factory', () => { let factory: ITerminalServiceFactory; let disposables: Disposable[] = []; let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let fs: TypeMoq.IMock<IFileSystem>; setup(() => { const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); const interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())).returns(() => interpreterService.object); + fs = TypeMoq.Mock.ofType<IFileSystem>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) + .returns(() => interpreterService.object); disposables = []; - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())).returns(() => disposables); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) + .returns(() => disposables); const terminalHelper = TypeMoq.Mock.ofType<ITerminalHelper>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalHelper), TypeMoq.It.isAny())).returns(() => terminalHelper.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalHelper), TypeMoq.It.isAny())) + .returns(() => terminalHelper.object); const terminalManager = TypeMoq.Mock.ofType<ITerminalManager>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalManager), TypeMoq.It.isAny())).returns(() => terminalManager.object); - factory = new TerminalServiceFactory(serviceContainer.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalManager), TypeMoq.It.isAny())) + .returns(() => terminalManager.object); + factory = new TerminalServiceFactory(serviceContainer.object, fs.object, interpreterService.object); workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())).returns(() => workspaceService.object); - + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) + .returns(() => workspaceService.object); }); teardown(() => { - disposables.forEach(disposable => { + disposables.forEach((disposable) => { if (disposable) { disposable.dispose(); } @@ -42,27 +54,37 @@ suite('Terminal Service Factory', () => { }); test('Ensure same instance of terminal service is returned', () => { - const instance = factory.getTerminalService(); - const sameInstance = factory.getTerminalService() === instance; + const instance = factory.getTerminalService({}) as SynchronousTerminalService; + const sameInstance = + (factory.getTerminalService({}) as SynchronousTerminalService).terminalService === instance.terminalService; expect(sameInstance).to.equal(true, 'Instances are not the same'); - const differentInstance = factory.getTerminalService(undefined, 'New Title'); + const 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(); - expect(defaultInstance instanceof TerminalService).to.equal(true, 'Not an instance of Terminal service'); - - const notSameAsDefaultInstance = factory.getTerminalService(undefined, 'New Title') === defaultInstance; + const defaultInstance = factory.getTerminalService({}); + expect(defaultInstance instanceof SynchronousTerminalService).to.equal( + true, + 'Not an instance of Terminal service', + ); + + 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'); - const sameInstance = factory.getTerminalService(undefined, 'New Title') === instance; + const instance = factory.getTerminalService({ + resource: undefined, + title: 'New Title', + }) as SynchronousTerminalService; + const sameInstance = + (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'); }); @@ -83,29 +105,84 @@ 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'); const workspaceUriA = Uri.file('A'); const workspaceUriB = Uri.file('B'); const workspaceFolderA = TypeMoq.Mock.ofType<WorkspaceFolder>(); - workspaceFolderA.setup(w => w.uri).returns(() => workspaceUriA); + workspaceFolderA.setup((w) => w.uri).returns(() => workspaceUriA); const workspaceFolderB = TypeMoq.Mock.ofType<WorkspaceFolder>(); - workspaceFolderB.setup(w => w.uri).returns(() => workspaceUriB); - - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(file1A))).returns(() => workspaceFolderA.object); - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(file2A))).returns(() => workspaceFolderA.object); - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(fileB))).returns(() => workspaceFolderB.object); - - const terminalForFile1A = factory.getTerminalService(file1A); - const terminalForFile2A = factory.getTerminalService(file2A); - const terminalForFileB = factory.getTerminalService(fileB); - - const terminalsAreSameForWorkspaceA = terminalForFile1A === terminalForFile2A; + 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 }) 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'); - const terminalsForWorkspaceABAreDifferent = terminalForFile1A === terminalForFileB; - expect(terminalsForWorkspaceABAreDifferent).to.equal(false, 'Instances should be different for different workspaces'); + const terminalsForWorkspaceABAreDifferent = + terminalForFile1A.terminalService === terminalForFileB.terminalService; + expect(terminalsForWorkspaceABAreDifferent).to.equal( + false, + '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.activation.unit.test.ts b/src/test/common/terminals/helper.activation.unit.test.ts deleted file mode 100644 index 380085b05f50..000000000000 --- a/src/test/common/terminals/helper.activation.unit.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import * as TypeMoq from 'typemoq'; -import { Disposable } from 'vscode'; -import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; -import { IPlatformService } from '../../../client/common/platform/types'; -import { Bash } from '../../../client/common/terminal/environmentActivationProviders/bash'; -import { CommandPromptAndPowerShell } from '../../../client/common/terminal/environmentActivationProviders/commandPrompt'; -import { TerminalHelper } from '../../../client/common/terminal/helper'; -import { ITerminalActivationCommandProvider, ITerminalHelper, TerminalShellType } from '../../../client/common/terminal/types'; -import { IConfigurationService, IDisposableRegistry, IPythonSettings, ITerminalSettings } from '../../../client/common/types'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; -import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { IServiceContainer } from '../../../client/ioc/types'; - -// tslint:disable-next-line:max-func-body-length -suite('Terminal Service helpers', () => { - let helper: ITerminalHelper; - let terminalManager: TypeMoq.IMock<ITerminalManager>; - let platformService: TypeMoq.IMock<IPlatformService>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - let disposables: Disposable[] = []; - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let interpreterService: TypeMoq.IMock<IInterpreterService>; - let terminalSettings: TypeMoq.IMock<ITerminalSettings>; - - setup(() => { - terminalManager = TypeMoq.Mock.ofType<ITerminalManager>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); - terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>(); - disposables = []; - - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - serviceContainer.setup(c => c.get(ITerminalManager)).returns(() => terminalManager.object); - serviceContainer.setup(c => c.get(IPlatformService)).returns(() => platformService.object); - serviceContainer.setup(c => c.get(IDisposableRegistry)).returns(() => disposables); - serviceContainer.setup(c => c.get(IWorkspaceService)).returns(() => workspaceService.object); - serviceContainer.setup(c => c.get(IInterpreterService)).returns(() => interpreterService.object); - - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - serviceContainer.setup(c => c.get(IConfigurationService)).returns(() => configService.object); - const settings = TypeMoq.Mock.ofType<IPythonSettings>(); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - settings.setup(s => s.terminal).returns(() => terminalSettings.object); - - const condaService = TypeMoq.Mock.ofType<ICondaService>(); - condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); - - helper = new TerminalHelper(serviceContainer.object); - }); - teardown(() => { - disposables.filter(item => !!item).forEach(item => item.dispose()); - }); - - test('Activation command is undefined when terminal activation is disabled', async () => { - terminalSettings.setup(t => t.activateEnvironment).returns(() => false); - const commands = await helper.getEnvironmentActivationCommands(TerminalShellType.other); - - expect(commands).to.equal(undefined, 'Activation command should be undefined if terminal type cannot be determined'); - }); - - test('Activation command is undefined for unknown terminal', async () => { - terminalSettings.setup(t => t.activateEnvironment).returns(() => true); - - const bashActivation = new Bash(serviceContainer.object); - const commandPromptActivation = new CommandPromptAndPowerShell(serviceContainer.object); - serviceContainer.setup(c => c.getAll(ITerminalActivationCommandProvider)).returns(() => [bashActivation, commandPromptActivation]); - const commands = await helper.getEnvironmentActivationCommands(TerminalShellType.other); - - expect(commands).to.equal(undefined, 'Activation command should be undefined if terminal type cannot be determined'); - }); -}); - -getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(terminalShell => { - suite(`Terminal Service helpers (${terminalShell.name})`, () => { - let helper: ITerminalHelper; - let terminalManager: TypeMoq.IMock<ITerminalManager>; - let platformService: TypeMoq.IMock<IPlatformService>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - let disposables: Disposable[] = []; - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let interpreterService: TypeMoq.IMock<IInterpreterService>; - - setup(() => { - terminalManager = TypeMoq.Mock.ofType<ITerminalManager>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); - disposables = []; - - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - serviceContainer.setup(c => c.get(ITerminalManager)).returns(() => terminalManager.object); - serviceContainer.setup(c => c.get(IPlatformService)).returns(() => platformService.object); - serviceContainer.setup(c => c.get(IDisposableRegistry)).returns(() => disposables); - serviceContainer.setup(c => c.get(IWorkspaceService)).returns(() => workspaceService.object); - serviceContainer.setup(c => c.get(IInterpreterService)).returns(() => interpreterService.object); - - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - serviceContainer.setup(c => c.get(IConfigurationService)).returns(() => configService.object); - const settings = TypeMoq.Mock.ofType<IPythonSettings>(); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - const terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>(); - settings.setup(s => s.terminal).returns(() => terminalSettings.object); - terminalSettings.setup(t => t.activateEnvironment).returns(() => true); - - const condaService = TypeMoq.Mock.ofType<ICondaService>(); - condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICondaService))).returns(() => condaService.object); - - helper = new TerminalHelper(serviceContainer.object); - }); - teardown(() => { - disposables.filter(disposable => !!disposable).forEach(disposable => disposable.dispose()); - }); - - async function activationCommandShouldReturnCorrectly(shellType: TerminalShellType, expectedActivationCommand?: string[]) { - // This will only work for the current shell type. - const validProvider = TypeMoq.Mock.ofType<ITerminalActivationCommandProvider>(); - validProvider.setup(p => p.isShellSupported(TypeMoq.It.isValue(shellType))).returns(() => true); - validProvider.setup(p => p.getActivationCommands(TypeMoq.It.isValue(undefined), TypeMoq.It.isValue(shellType))).returns(() => Promise.resolve(expectedActivationCommand)); - - // This will support other providers. - const invalidProvider = TypeMoq.Mock.ofType<ITerminalActivationCommandProvider>(); - invalidProvider.setup(p => p.isShellSupported(TypeMoq.It.isAny())).returns(item => shellType !== shellType); - - serviceContainer.setup(c => c.getAll(ITerminalActivationCommandProvider)).returns(() => [validProvider.object, invalidProvider.object]); - const commands = await helper.getEnvironmentActivationCommands(shellType); - - validProvider.verify(p => p.getActivationCommands(TypeMoq.It.isValue(undefined), TypeMoq.It.isValue(shellType)), TypeMoq.Times.once()); - validProvider.verify(p => p.isShellSupported(TypeMoq.It.isValue(shellType)), TypeMoq.Times.once()); - invalidProvider.verify(p => p.getActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - invalidProvider.verify(p => p.isShellSupported(TypeMoq.It.isValue(shellType)), TypeMoq.Times.once()); - - expect(commands).to.deep.equal(expectedActivationCommand, 'Incorrect activation command'); - } - - test(`Activation command should be correctly identified for ${terminalShell.name} (command array)`, async () => { - await activationCommandShouldReturnCorrectly(terminalShell.value, ['a', 'b']); - }); - test(`Activation command should be correctly identified for ${terminalShell.name} (command string)`, async () => { - await activationCommandShouldReturnCorrectly(terminalShell.value, ['command to be executed']); - }); - test(`Activation command should be correctly identified for ${terminalShell.name} (undefined)`, async () => { - await activationCommandShouldReturnCorrectly(terminalShell.value); - }); - - async function activationCommandShouldReturnUndefined(shellType: TerminalShellType) { - // This will support other providers. - const invalidProvider = TypeMoq.Mock.ofType<ITerminalActivationCommandProvider>(); - invalidProvider.setup(p => p.isShellSupported(TypeMoq.It.isAny())).returns(item => shellType !== shellType); - - serviceContainer.setup(c => c.getAll(ITerminalActivationCommandProvider)).returns(() => [invalidProvider.object]); - const commands = await helper.getEnvironmentActivationCommands(shellType); - - invalidProvider.verify(p => p.getActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - expect(commands).to.deep.equal(undefined, 'Incorrect activation command'); - } - - test(`Activation command should return undefined ${terminalShell.name} (no matching providers)`, async () => { - await activationCommandShouldReturnUndefined(terminalShell.value); - }); - }); -}); diff --git a/src/test/common/terminals/helper.unit.test.ts b/src/test/common/terminals/helper.unit.test.ts index b06011efa2d3..0d130b573408 100644 --- a/src/test/common/terminals/helper.unit.test.ts +++ b/src/test/common/terminals/helper.unit.test.ts @@ -1,146 +1,458 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. - import { expect } from 'chai'; -import * as TypeMoq from 'typemoq'; -import { Disposable, WorkspaceConfiguration } from 'vscode'; -import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; +import { SemVer } from 'semver'; +import * as sinon from 'sinon'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { Terminal, Uri } from 'vscode'; +import { TerminalManager } from '../../../client/common/application/terminalManager'; +import { ITerminalManager } from '../../../client/common/application/types'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { ConfigurationService } from '../../../client/common/configuration/service'; +import { PlatformService } from '../../../client/common/platform/platformService'; import { IPlatformService } from '../../../client/common/platform/types'; +import { Bash } from '../../../client/common/terminal/environmentActivationProviders/bash'; +import { CommandPromptAndPowerShell } from '../../../client/common/terminal/environmentActivationProviders/commandPrompt'; +import { 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'; import { TerminalHelper } from '../../../client/common/terminal/helper'; -import { ITerminalHelper, TerminalShellType } from '../../../client/common/terminal/types'; -import { IDisposableRegistry } from '../../../client/common/types'; +import { ShellDetector } from '../../../client/common/terminal/shellDetector'; +import { TerminalNameShellDetector } from '../../../client/common/terminal/shellDetectors/terminalNameShellDetector'; +import { + IShellDetector, + ITerminalActivationCommandProvider, + TerminalShellType, +} from '../../../client/common/terminal/types'; +import { IConfigurationService } from '../../../client/common/types'; import { getNamesAndValues } from '../../../client/common/utils/enum'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { Architecture, OSType } from '../../../client/common/utils/platform'; +import { IComponentAdapter } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; import { IServiceContainer } from '../../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { PixiActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pixiActivationProvider'; -// tslint:disable-next-line:max-func-body-length suite('Terminal Service helpers', () => { - let helper: ITerminalHelper; - let terminalManager: TypeMoq.IMock<ITerminalManager>; - let platformService: TypeMoq.IMock<IPlatformService>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - let disposables: Disposable[] = []; - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let interpreterService: TypeMoq.IMock<IInterpreterService>; - - setup(() => { - terminalManager = TypeMoq.Mock.ofType<ITerminalManager>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); - disposables = []; - - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - serviceContainer.setup(c => c.get(ITerminalManager)).returns(() => terminalManager.object); - serviceContainer.setup(c => c.get(IPlatformService)).returns(() => platformService.object); - serviceContainer.setup(c => c.get(IDisposableRegistry)).returns(() => disposables); - serviceContainer.setup(c => c.get(IWorkspaceService)).returns(() => workspaceService.object); - serviceContainer.setup(c => c.get(IInterpreterService)).returns(() => interpreterService.object); - - helper = new TerminalHelper(serviceContainer.object); - }); - teardown(() => { - disposables.filter(item => !!item).forEach(item => item.dispose()); - }); + let helper: TerminalHelper; + let terminalManager: ITerminalManager; + let platformService: IPlatformService; + 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: PythonEnvironment = { + path: '/foo/bar/python.exe', + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, + }; - test('Test identification of Terminal Shells', async () => { - const shellPathsAndIdentification = new Map<string, TerminalShellType>(); - shellPathsAndIdentification.set('c:\\windows\\system32\\cmd.exe', TerminalShellType.commandPrompt); + function doSetup() { + mockDetector = mock(TerminalNameShellDetector); + terminalManager = mock(TerminalManager); + platformService = mock(PlatformService); + 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(serviceContainer), + instance(mock(InterpreterService)), + instance(configurationService), + instance(condaActivationProvider), + instance(bashActivationProvider), + instance(cmdActivationProvider), + instance(nushellActivationProvider), + instance(pyenvActivationProvider), + instance(pipenvActivationProvider), + 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'); + }); - shellPathsAndIdentification.set('c:\\windows\\system32\\bash.exe', TerminalShellType.bash); - shellPathsAndIdentification.set('c:\\windows\\system32\\wsl.exe', TerminalShellType.wsl); - shellPathsAndIdentification.set('c:\\windows\\system32\\gitbash.exe', TerminalShellType.gitbash); - shellPathsAndIdentification.set('/usr/bin/bash', TerminalShellType.bash); - shellPathsAndIdentification.set('/usr/bin/zsh', TerminalShellType.zsh); - shellPathsAndIdentification.set('/usr/bin/ksh', TerminalShellType.ksh); + 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); - shellPathsAndIdentification.set('c:\\windows\\system32\\powershell.exe', TerminalShellType.powershell); - shellPathsAndIdentification.set('c:\\windows\\system32\\pwsh.exe', TerminalShellType.powershellCore); - shellPathsAndIdentification.set('/usr/microsoft/xxx/powershell/powershell', TerminalShellType.powershell); - shellPathsAndIdentification.set('/usr/microsoft/xxx/powershell/pwsh', TerminalShellType.powershellCore); + const term = helper.createTerminal(theTitle); - shellPathsAndIdentification.set('/usr/bin/fish', TerminalShellType.fish); + 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); + }); - shellPathsAndIdentification.set('c:\\windows\\system32\\shell.exe', TerminalShellType.other); - shellPathsAndIdentification.set('/usr/bin/shell', TerminalShellType.other); + test('Create terminal without a title', () => { + const terminal = 'Terminal Created'; + when(terminalManager.createTerminal(anything())).thenReturn(terminal as any); - shellPathsAndIdentification.set('/usr/bin/csh', TerminalShellType.cshell); - shellPathsAndIdentification.set('/usr/bin/tcsh', TerminalShellType.tcshell); + const term = helper.createTerminal(); - shellPathsAndIdentification.forEach((shellType, shellPath) => { - expect(helper.identifyTerminalShell(shellPath)).to.equal(shellType, `Incorrect Shell Type for path '${shellPath}'`); + verify(terminalManager.createTerminal(anything())).once(); + const args = capture(terminalManager.createTerminal).first()[0]; + expect(term).to.be.deep.equal(terminal); + expect(args.name).to.be.deep.equal(undefined, 'name should be undefined'); }); - }); + test('Create terminal with a title', () => { + const theTitle = 'Hello'; + const terminal = 'Terminal Created'; + when(terminalManager.createTerminal(anything())).thenReturn(terminal as any); + + const term = helper.createTerminal(theTitle); - async function ensurePathForShellIsCorrectlyRetrievedFromSettings(os: 'windows' | 'osx' | 'linux', expectedShellPat: string) { - const shellPath = 'abcd'; - workspaceService.setup(w => w.getConfiguration(TypeMoq.It.isValue('terminal.integrated.shell'))).returns(() => { - const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - workspaceConfig.setup(c => c.get(os)).returns(() => shellPath); - return workspaceConfig.object; + verify(terminalManager.createTerminal(anything())).once(); + const args = capture(terminalManager.createTerminal).first()[0]; + expect(term).to.be.deep.equal(terminal); + expect(args.name).to.be.deep.equal(theTitle); }); + test('Ensure spaces in command is quoted', async () => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((item) => { + const command = 'c:\\python 3.7.exe'; + const args = ['1', '2']; + const commandPrefix = + item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore + ? '& ' + : ''; + const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgumentForPythonExt()} 1 2`; - platformService.setup(p => p.isWindows).returns(() => os === 'windows'); - platformService.setup(p => p.isLinux).returns(() => os === 'linux'); - platformService.setup(p => p.isMac).returns(() => os === 'osx'); - expect(helper.getTerminalShellPath()).to.equal(shellPath, 'Incorrect path for Osx'); - } - test('Ensure path for shell is correctly retrieved from settings (osx)', async () => { - await ensurePathForShellIsCorrectlyRetrievedFromSettings('osx', 'abcd'); - }); - test('Ensure path for shell is correctly retrieved from settings (linux)', async () => { - await ensurePathForShellIsCorrectlyRetrievedFromSettings('linux', 'abcd'); - }); - test('Ensure path for shell is correctly retrieved from settings (windows)', async () => { - await ensurePathForShellIsCorrectlyRetrievedFromSettings('windows', 'abcd'); - }); - test('Ensure path for shell is correctly retrieved from settings (unknown os)', async () => { - await ensurePathForShellIsCorrectlyRetrievedFromSettings('windows', ''); - }); + const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); + expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); + }); + }); + test('Ensure spaces in args are quoted', async () => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((item) => { + const command = 'python3.7.exe'; + const args = ['a file.py', '1', '2']; + const commandPrefix = + item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore + ? '& ' + : ''; + const expectedTerminalCommand = `${commandPrefix}${command} "a file.py" 1 2`; + + const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); + expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); + }); + }); - test('Ensure spaces in command is quoted', async () => { - getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(item => { - const command = 'c:\\python 3.7.exe'; - const args = ['1', '2']; - const commandPrefix = (item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore) ? '& ' : ''; - const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgument()} 1 2`; + test('Ensure empty args are ignored', async () => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((item) => { + const command = 'python3.7.exe'; + const args: string[] = []; + const commandPrefix = + item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore + ? '& ' + : ''; + const expectedTerminalCommand = `${commandPrefix}${command}`; - const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); - expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); + const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); + expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell '${item.name}'`); + }); }); - }); - test('Ensure empty args are ignored', async () => { - getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(item => { - const command = 'python3.7.exe'; - const args = []; - const commandPrefix = (item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore) ? '& ' : ''; - const expectedTerminalCommand = `${commandPrefix}${command}`; + test('Ensure empty args are ignored with s in command', async () => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((item) => { + const command = 'c:\\python 3.7.exe'; + const args: string[] = []; + const commandPrefix = + item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore + ? '& ' + : ''; + const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgumentForPythonExt()}`; - const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); - expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell '${item.name}'`); + const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); + expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); + }); }); }); - test('Ensure empty args are ignored with s in command', async () => { - getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(item => { - const command = 'c:\\python 3.7.exe'; - const args = []; - const commandPrefix = (item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore) ? '& ' : ''; - const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgument()}`; + function title(resource?: Uri, interpreter?: PythonEnvironment) { + return `${resource ? 'With a resource' : 'Without a resource'}${interpreter ? ' and an interpreter' : ''}`; + } + + suite('Activation', () => { + [undefined, Uri.parse('a')].forEach((resource) => { + suite(title(resource), () => { + setup(() => { + doSetup(); + when(configurationService.getSettings(resource)).thenReturn(instance(pythonSettings)); + }); + function ensureCondaIsSupported( + isSupported: boolean, + pythonPath: string, + condaActivationCommands: string[], + ) { + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(pythonSettings.terminal).thenReturn({ activateEnvironment: true } as any); + when(condaService.isCondaEnvironment(pythonPath)).thenResolve(isSupported); + when(condaActivationProvider.getActivationCommands(resource, anything())).thenResolve( + condaActivationCommands, + ); + } + test('Activation command must return conda activation command if interpreter is conda', async () => { + const pythonPath = 'some python Path value'; + const condaActivationCommands = ['Hello', '1']; + ensureCondaIsSupported(true, pythonPath, condaActivationCommands); + + const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); + + expect(cmd).to.equal(condaActivationCommands); + verify(pythonSettings.pythonPath).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 () => { + const pythonPath = 'some python Path value'; + ensureCondaIsSupported(false, pythonPath, []); + + 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, + ); + + expect(cmd).to.equal(undefined, 'Command must be undefined'); + 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); + }); + test('Activation command must return command from bash if that is supported and others are not', async () => { + const pythonPath = 'some python Path value'; + const expectCommand = ['one', 'two']; + ensureCondaIsSupported(false, pythonPath, []); - const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); - expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); + when(bashActivationProvider.getActivationCommands(resource, anything())).thenResolve(expectCommand); + + 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).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); + }); + test('Activation command must return command from pipenv if that is supported and even if others are supported', async () => { + const pythonPath = 'some python Path value'; + const expectCommand = ['one', 'two']; + ensureCondaIsSupported(false, pythonPath, []); + + when(pipenvActivationProvider.getActivationCommands(resource, anything())).thenResolve( + expectCommand, + ); + when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(true); + + [ + bashActivationProvider, + cmdActivationProvider, + nushellActivationProvider, + pyenvActivationProvider, + ].forEach((provider) => { + when(provider.getActivationCommands(resource, anything())).thenResolve(['Something']); + when(provider.isShellSupported(anything())).thenReturn(true); + }); + + const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); + + expect(cmd).to.deep.equal(expectCommand); + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); + verify(bashActivationProvider.getActivationCommands(resource, anything())).never(); + verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(pipenvActivationProvider.getActivationCommands(resource, anything())).atLeast(1); + verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); + 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'; + const expectCommand = ['one', 'two']; + ensureCondaIsSupported(false, pythonPath, []); + + when(cmdActivationProvider.getActivationCommands(resource, anything())).thenResolve(expectCommand); + + when(bashActivationProvider.isShellSupported(anything())).thenReturn(false); + when(cmdActivationProvider.isShellSupported(anything())).thenReturn(true); + when(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).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 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).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).once(); + 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 () => { + when(platformService.osType).thenReturn(OSType.Unknown); + + for (const item of getNamesAndValues<TerminalShellType>(TerminalShellType)) { + const cmd = await helper.getEnvironmentActivationShellCommands( + resource, + item.value, + interpreter, + ); + expect(cmd).to.equal(undefined, 'Command must be undefined'); + } + }); + }); + [undefined, pythonInterpreter].forEach((interpreter) => { + [OSType.Linux, OSType.OSX, OSType.Windows].forEach((osType) => { + test(`Activation command for Shell must never use pipenv nor pyenv (${osType})`, async () => { + const pythonPath = 'some python Path value'; + const shellToExpect = + osType === OSType.Windows ? TerminalShellType.commandPrompt : TerminalShellType.bash; + ensureCondaIsSupported(false, pythonPath, []); + + shellDetectorIdentifyTerminalShell.returns(shellToExpect); + when(platformService.osType).thenReturn(osType); + when(bashActivationProvider.isShellSupported(shellToExpect)).thenReturn(false); + when(cmdActivationProvider.isShellSupported(shellToExpect)).thenReturn(false); + when(nushellActivationProvider.isShellSupported(shellToExpect)).thenReturn(false); + + const cmd = await helper.getEnvironmentActivationShellCommands( + resource, + shellToExpect, + interpreter, + ); + + expect(cmd).to.equal(undefined, 'Command must be undefined'); + 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); + }); + }); + }); + }); }); }); - test('Ensure a terminal is created (without a title)', () => { - helper.createTerminal(); - terminalManager.verify(t => t.createTerminal(TypeMoq.It.isValue({ name: undefined })), TypeMoq.Times.once()); - }); + suite('Identify Terminal Shell', () => { + setup(doSetup); + test('Use shell detector to identify terminal shells', () => { + const terminal = {} as any; + const expectedShell = TerminalShellType.ksh; + shellDetectorIdentifyTerminalShell.returns(expectedShell); + + const shell = helper.identifyTerminalShell(terminal); + expect(shell).to.be.equal(expectedShell); + expect(shellDetectorIdentifyTerminalShell.callCount).to.equal(1); + expect(shellDetectorIdentifyTerminalShell.args[0]).deep.equal([terminal]); + }); + test('Detector passed throught constructor is used by shell detector class', () => { + const terminal = {} as any; + const expectedShell = TerminalShellType.ksh; + shellDetectorIdentifyTerminalShell.callThrough(); + when(mockDetector.identify(anything(), terminal)).thenReturn(expectedShell); + + const shell = helper.identifyTerminalShell(terminal); - test('Ensure a terminal is created with the provided title', () => { - helper.createTerminal('1234'); - terminalManager.verify(t => t.createTerminal(TypeMoq.It.isValue({ name: '1234' })), TypeMoq.Times.once()); + expect(shell).to.be.equal(expectedShell); + expect(shellDetectorIdentifyTerminalShell.callCount).to.equal(1); + verify(mockDetector.identify(anything(), terminal)).once(); + }); }); }); diff --git a/src/test/common/terminals/pyenvActivationProvider.unit.test.ts b/src/test/common/terminals/pyenvActivationProvider.unit.test.ts index 5acc4880f854..404425791580 100644 --- a/src/test/common/terminals/pyenvActivationProvider.unit.test.ts +++ b/src/test/common/terminals/pyenvActivationProvider.unit.test.ts @@ -11,8 +11,9 @@ import { PyEnvActivationCommandProvider } from '../../../client/common/terminal/ import { ITerminalActivationCommandProvider, TerminalShellType } from '../../../client/common/terminal/types'; import { getNamesAndValues } from '../../../client/common/utils/enum'; import { Architecture } from '../../../client/common/utils/platform'; -import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; suite('Terminal Environment Activation pyenv', () => { let serviceContainer: TypeMoq.IMock<IServiceContainer>; @@ -22,7 +23,9 @@ suite('Terminal Environment Activation pyenv', () => { setup(() => { serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())).returns(() => interpreterService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) + .returns(() => interpreterService.object); activationProvider = new PyEnvActivationCommandProvider(serviceContainer.object); }); @@ -35,7 +38,7 @@ suite('Terminal Environment Activation pyenv', () => { test('Ensure no activation commands are returned if intrepreter info is not found', async () => { interpreterService - .setup(i => i.getActiveInterpreter(TypeMoq.It.isAny())) + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) .returns(() => Promise.resolve(undefined)) .verifiable(TypeMoq.Times.once()); @@ -44,16 +47,16 @@ 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())) + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) .returns(() => Promise.resolve(intepreterInfo)) .verifiable(TypeMoq.Times.once()); @@ -62,16 +65,16 @@ 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())) + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) .returns(() => Promise.resolve(intepreterInfo)) .verifiable(TypeMoq.Times.once()); @@ -80,21 +83,24 @@ 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())) + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) .returns(() => Promise.resolve(intepreterInfo)) .verifiable(TypeMoq.Times.once()); const activationCommands = await activationProvider.getActivationCommands(undefined, TerminalShellType.bash); - expect(activationCommands).to.deep.equal([`pyenv shell ${intepreterInfo.envName}`], 'Invalid Activation command'); + expect(activationCommands).to.deep.equal( + [`pyenv shell "${intepreterInfo.envName}"`], + 'Invalid Activation command', + ); }); }); diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts index 24814c12decb..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,122 +46,420 @@ 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>(); - 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); + 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()); + disposables.filter((item) => !!item).forEach((item) => item.dispose()); + sinon.restore(); + interpreterService.reset(); }); test('Ensure terminal is disposed', async () => { - terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); const os: string = 'windows'; service = new TerminalService(mockServiceContainer.object); const shellPath = 'powershell.exe'; - workspaceService.setup(w => w.getConfiguration(TypeMoq.It.isValue('terminal.integrated.shell'))).returns(() => { - const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - workspaceConfig.setup(c => c.get(os)).returns(() => shellPath); - return workspaceConfig.object; - }); - - platformService.setup(p => p.isWindows).returns(() => os === 'windows'); - platformService.setup(p => p.isLinux).returns(() => os === 'linux'); - platformService.setup(p => p.isMac).returns(() => os === 'osx'); - terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); - terminalHelper.setup(h => h.buildCommandForTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => 'dummy text'); + // 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(() => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + 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'); + platformService.setup((p) => p.isMac).returns(() => os === 'osx'); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + terminalHelper + .setup((h) => h.buildCommandForTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => 'dummy text'); + 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)); + terminal.verify((t) => t.dispose(), TypeMoq.Times.exactly(1)); }); test('Ensure command is sent to terminal and it is shown', async () => { - terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + 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)); service = new TerminalService(mockServiceContainer.object); const commandToSend = 'SomeCommand'; const args = ['1', '2']; const commandToExpect = [commandToSend].concat(args).join(' '); - terminalHelper.setup(h => h.buildCommandForTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => commandToExpect); - terminalHelper.setup(h => h.getTerminalShellPath()).returns(() => ''); - terminalHelper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); - terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + terminalHelper + .setup((h) => h.buildCommandForTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => commandToExpect); + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.sendCommand(commandToSend, args); - terminal.verify(t => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(2)); - terminal.verify(t => t.sendText(TypeMoq.It.isValue(commandToExpect), TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + 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.never(), + ); }); test('Ensure text is sent to terminal and it is shown', async () => { - terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + 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.getTerminalShellPath()).returns(() => ''); - terminalHelper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); - terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.sendText(textToSend); - terminal.verify(t => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(2)); - terminal.verify(t => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(2)); + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + + test('Ensure 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 terminal shown', async () => { - terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + 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); - terminalHelper.setup(h => h.getTerminalShellPath()).returns(() => ''); - terminalHelper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); - terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.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())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object, { hideFromUser: true }); + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.show(); - terminal.verify(t => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(2)); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.never()); + }); + + test('Ensure terminal shown otherwise', async () => { + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.show(); + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(2)); }); test('Ensure terminal shown and focus is set to the Terminal', async () => { - terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); service = new TerminalService(mockServiceContainer.object); - terminalHelper.setup(h => h.getTerminalShellPath()).returns(() => ''); - terminalHelper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); - terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); await service.show(false); - terminal.verify(t => t.show(TypeMoq.It.isValue(false)), TypeMoq.Times.exactly(2)); + terminal.verify((t) => t.show(TypeMoq.It.isValue(false)), TypeMoq.Times.exactly(2)); }); - test('Ensure terminal is activated once after creation', async () => { + test('Ensure PYTHONSTARTUP is injected', async () => { service = new TerminalService(mockServiceContainer.object); - terminalHelper - .setup(h => h.getTerminalShellPath()).returns(() => '') + 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 - .setup(h => h.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .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) + .setup((t) => t.createTerminal(TypeMoq.It.isAny())) + .returns(() => terminal.object) .verifiable(TypeMoq.Times.atLeastOnce()); await service.show(); @@ -147,21 +469,19 @@ suite('Terminal Service', () => { terminalHelper.verifyAll(); terminalActivator.verifyAll(); - terminal.verify(t => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); }); test('Ensure terminal is activated once before sending text', async () => { service = new TerminalService(mockServiceContainer.object); const textToSend = 'Some Text'; - terminalHelper - .setup(h => h.getTerminalShellPath()).returns(() => '') - .verifiable(TypeMoq.Times.once()); terminalActivator - .setup(h => h.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .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) + .setup((t) => t.createTerminal(TypeMoq.It.isAny())) + .returns(() => terminal.object) .verifiable(TypeMoq.Times.atLeastOnce()); await service.sendText(textToSend); @@ -171,23 +491,26 @@ suite('Terminal Service', () => { terminalHelper.verifyAll(); terminalActivator.verifyAll(); - terminal.verify(t => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); }); test('Ensure close event is not fired when another terminal is closed', async () => { - terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); let eventFired = false; let eventHandler: undefined | (() => void); - terminalManager.setup(m => m.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { - eventHandler = handler; - // tslint:disable-next-line:no-empty - return { dispose: () => { } }; - }); + terminalManager + .setup((m) => m.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((handler) => { + eventHandler = handler; + + return { dispose: () => {} }; + }); service = new TerminalService(mockServiceContainer.object); - service.onDidCloseTerminal(() => eventFired = true, service); - terminalHelper.setup(h => h.getTerminalShellPath()).returns(() => ''); - terminalHelper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); - terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + service.onDidCloseTerminal(() => (eventFired = true), service); + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); // This will create the terminal. await service.sendText('blah'); @@ -198,20 +521,23 @@ suite('Terminal Service', () => { }); test('Ensure close event is not fired when terminal is closed', async () => { - terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); let eventFired = false; let eventHandler: undefined | ((t: VSCodeTerminal) => void); - terminalManager.setup(m => m.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { - eventHandler = handler; - // tslint:disable-next-line:no-empty - return { dispose: () => { } }; - }); + terminalManager + .setup((m) => m.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((handler) => { + eventHandler = handler; + + return { dispose: () => {} }; + }); service = new TerminalService(mockServiceContainer.object); - service.onDidCloseTerminal(() => eventFired = true); + service.onDidCloseTerminal(() => (eventFired = true)); - terminalHelper.setup(h => h.getTerminalShellPath()).returns(() => ''); - terminalHelper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); - terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + 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'); @@ -220,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 new file mode 100644 index 000000000000..c6c03fec05a1 --- /dev/null +++ b/src/test/common/terminals/serviceRegistry.unit.test.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; +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 { registerTypes } from '../../../client/terminals/serviceRegistry'; +import { + ICodeExecutionHelper, + ICodeExecutionManager, + ICodeExecutionService, + ITerminalAutoActivation, +} from '../../../client/terminals/types'; + +suite('Common Terminal Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager)); + verify(serviceManager.addSingleton<ICodeExecutionHelper>(ICodeExecutionHelper, CodeExecutionHelper)).once(); + + verify(serviceManager.addSingleton<ICodeExecutionManager>(ICodeExecutionManager, CodeExecutionManager)).once(); + + verify( + serviceManager.addSingleton<ICodeExecutionService>( + ICodeExecutionService, + DjangoShellCodeExecutionProvider, + 'djangoShell', + ), + ).once(); + verify( + serviceManager.addSingleton<ICodeExecutionService>( + ICodeExecutionService, + TerminalCodeExecutionProvider, + 'standard', + ), + ).once(); + verify(serviceManager.addSingleton<ICodeExecutionService>(ICodeExecutionService, ReplProvider, 'repl')).once(); + + verify( + serviceManager.addSingleton<ITerminalAutoActivation>(ITerminalAutoActivation, TerminalAutoActivation), + ).once(); + }); +}); diff --git a/src/test/common/terminals/shellDetector.unit.test.ts b/src/test/common/terminals/shellDetector.unit.test.ts new file mode 100644 index 000000000000..c09560a3ea37 --- /dev/null +++ b/src/test/common/terminals/shellDetector.unit.test.ts @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { ApplicationEnvironment } from '../../../client/common/application/applicationEnvironment'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { ShellDetector } from '../../../client/common/terminal/shellDetector'; +import { SettingsShellDetector } from '../../../client/common/terminal/shellDetectors/settingsShellDetector'; +import { TerminalNameShellDetector } from '../../../client/common/terminal/shellDetectors/terminalNameShellDetector'; +import { UserEnvironmentShellDetector } from '../../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; +import { VSCEnvironmentShellDetector } from '../../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; +import { TerminalShellType } from '../../../client/common/terminal/types'; +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { OSType } from '../../../client/common/utils/platform'; +import { MockProcess } from '../../../test/mocks/process'; + +suite('Shell Detector', () => { + let platformService: IPlatformService; + const defaultOSShells = { + [OSType.Linux]: TerminalShellType.bash, + [OSType.OSX]: TerminalShellType.bash, + [OSType.Windows]: TerminalShellType.commandPrompt, + [OSType.Unknown]: TerminalShellType.other, + }; + const sandbox = sinon.createSandbox(); + setup(() => (platformService = mock(PlatformService))); + teardown(() => sandbox.restore()); + + getNamesAndValues<OSType>(OSType).forEach((os) => { + const testSuffix = `(OS ${os.name})`; + test(`Test identification of Terminal Shells in order of priority ${testSuffix}`, async () => { + const callOrder: string[] = []; + const nameDetectorIdentify = sandbox.stub(TerminalNameShellDetector.prototype, 'identify'); + nameDetectorIdentify.callsFake(() => { + callOrder.push('calledFirst'); + return undefined; + }); + const vscEnvDetectorIdentify = sandbox.stub(VSCEnvironmentShellDetector.prototype, 'identify'); + vscEnvDetectorIdentify.callsFake(() => { + callOrder.push('calledSecond'); + return undefined; + }); + const userEnvDetectorIdentify = sandbox.stub(UserEnvironmentShellDetector.prototype, 'identify'); + userEnvDetectorIdentify.callsFake(() => { + callOrder.push('calledLast'); + return undefined; + }); + const settingsDetectorIdentify = sandbox.stub(SettingsShellDetector.prototype, 'identify'); + settingsDetectorIdentify.callsFake(() => { + callOrder.push('calledThird'); + return undefined; + }); + + when(platformService.osType).thenReturn(os.value); + const nameDetector = new TerminalNameShellDetector(); + const vscEnvDetector = new VSCEnvironmentShellDetector(instance(mock(ApplicationEnvironment))); + const userEnvDetector = new UserEnvironmentShellDetector(mock(MockProcess), instance(platformService)); + const settingsDetector = new SettingsShellDetector( + instance(mock(WorkspaceService)), + instance(platformService), + ); + const detectors = [settingsDetector, userEnvDetector, nameDetector, vscEnvDetector]; + const shellDetector = new ShellDetector(instance(platformService), detectors); + + shellDetector.identifyTerminalShell(); + + expect(callOrder).to.deep.equal(['calledFirst', 'calledSecond', 'calledThird', 'calledLast']); + }); + test(`Use default shell based on OS if there are no shell detectors ${testSuffix}`, () => { + when(platformService.osType).thenReturn(os.value); + when(platformService.osType).thenReturn(os.value); + const shellDetector = new ShellDetector(instance(platformService), []); + + const shell = shellDetector.identifyTerminalShell(); + + expect(shell).to.be.equal(defaultOSShells[os.value]); + }); + test(`Use default shell based on OS if there are no shell detectors (when a terminal is provided) ${testSuffix}`, () => { + when(platformService.osType).thenReturn(os.value); + const shellDetector = new ShellDetector(instance(platformService), []); + + const shell = shellDetector.identifyTerminalShell({ name: 'bash' } as any); + + expect(shell).to.be.equal(defaultOSShells[os.value]); + }); + test(`Use shell provided by detector ${testSuffix}`, () => { + when(platformService.osType).thenReturn(os.value); + const detector = mock(UserEnvironmentShellDetector); + const detectedShell = TerminalShellType.xonsh; + when(detector.identify(anything(), anything())).thenReturn(detectedShell); + const shellDetector = new ShellDetector(instance(platformService), [instance(detector)]); + + const shell = shellDetector.identifyTerminalShell(); + + expect(shell).to.be.equal(detectedShell); + verify(detector.identify(anything(), undefined)).once(); + }); + test(`Use shell provided by detector (when a terminal is provided) ${testSuffix}`, () => { + when(platformService.osType).thenReturn(os.value); + const terminal = { name: 'bash' } as any; + const detector = mock(UserEnvironmentShellDetector); + const detectedShell = TerminalShellType.xonsh; + when(detector.identify(anything(), anything())).thenReturn(detectedShell); + const shellDetector = new ShellDetector(instance(platformService), [instance(detector)]); + + const shell = shellDetector.identifyTerminalShell(terminal); + + expect(shell).to.be.equal(detectedShell); + verify(detector.identify(anything(), terminal)).once(); + }); + test(`Use shell provided by detector with highest priority ${testSuffix}`, () => { + when(platformService.osType).thenReturn(os.value); + const detector1 = mock(UserEnvironmentShellDetector); + const detector2 = mock(UserEnvironmentShellDetector); + const detector3 = mock(UserEnvironmentShellDetector); + const detectedShell = TerminalShellType.xonsh; + when(detector1.priority).thenReturn(0); + when(detector2.priority).thenReturn(2); + when(detector3.priority).thenReturn(1); + when(detector1.identify(anything(), anything())).thenReturn(TerminalShellType.tcshell); + when(detector2.identify(anything(), anything())).thenReturn(detectedShell); + when(detector3.identify(anything(), anything())).thenReturn(TerminalShellType.fish); + const shellDetector = new ShellDetector(instance(platformService), [ + instance(detector1), + instance(detector2), + instance(detector3), + ]); + + const shell = shellDetector.identifyTerminalShell(); + + expect(shell).to.be.equal(detectedShell); + verify(detector1.identify(anything(), anything())).never(); + verify(detector2.identify(anything(), undefined)).once(); + verify(detector3.identify(anything(), anything())).never(); + }); + test(`Fall back to detectors that can identify a shell ${testSuffix}`, () => { + when(platformService.osType).thenReturn(os.value); + const detector1 = mock(UserEnvironmentShellDetector); + const detector2 = mock(UserEnvironmentShellDetector); + const detector3 = mock(UserEnvironmentShellDetector); + const detector4 = mock(UserEnvironmentShellDetector); + const detectedShell = TerminalShellType.xonsh; + when(detector1.priority).thenReturn(1); + when(detector2.priority).thenReturn(2); + when(detector3.priority).thenReturn(3); + when(detector4.priority).thenReturn(4); + when(detector1.identify(anything(), anything())).thenReturn(TerminalShellType.ksh); + when(detector2.identify(anything(), anything())).thenReturn(detectedShell); + when(detector3.identify(anything(), anything())).thenReturn(undefined); + when(detector4.identify(anything(), anything())).thenReturn(undefined); + const shellDetector = new ShellDetector(instance(platformService), [ + instance(detector1), + instance(detector2), + instance(detector3), + instance(detector4), + ]); + + const shell = shellDetector.identifyTerminalShell(); + + expect(shell).to.be.equal(detectedShell); + verify(detector1.identify(anything(), anything())).never(); + verify(detector2.identify(anything(), undefined)).once(); + verify(detector3.identify(anything(), anything())).once(); + verify(detector4.identify(anything(), anything())).once(); + }); + test(`Fall back to detectors that can identify a shell ${testSuffix} (even if detected shell is other)`, () => { + when(platformService.osType).thenReturn(os.value); + const detector1 = mock(UserEnvironmentShellDetector); + const detector2 = mock(UserEnvironmentShellDetector); + const detector3 = mock(UserEnvironmentShellDetector); + const detector4 = mock(UserEnvironmentShellDetector); + const detectedShell = TerminalShellType.xonsh; + when(detector1.priority).thenReturn(1); + when(detector2.priority).thenReturn(2); + when(detector3.priority).thenReturn(3); + when(detector4.priority).thenReturn(4); + when(detector1.identify(anything(), anything())).thenReturn(TerminalShellType.ksh); + when(detector2.identify(anything(), anything())).thenReturn(detectedShell); + when(detector3.identify(anything(), anything())).thenReturn(TerminalShellType.other); + when(detector4.identify(anything(), anything())).thenReturn(TerminalShellType.other); + const shellDetector = new ShellDetector(instance(platformService), [ + instance(detector1), + instance(detector2), + instance(detector3), + instance(detector4), + ]); + + const shell = shellDetector.identifyTerminalShell(); + + expect(shell).to.be.equal(detectedShell); + verify(detector1.identify(anything(), anything())).never(); + verify(detector2.identify(anything(), undefined)).once(); + verify(detector3.identify(anything(), anything())).once(); + verify(detector4.identify(anything(), anything())).once(); + }); + }); +}); diff --git a/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts b/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts new file mode 100644 index 000000000000..e58e455ea7eb --- /dev/null +++ b/src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { instance, mock, when } from 'ts-mockito'; +import { Terminal } from 'vscode'; +import { ApplicationEnvironment } from '../../../../client/common/application/applicationEnvironment'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { PlatformService } from '../../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { CurrentProcess } from '../../../../client/common/process/currentProcess'; +import { SettingsShellDetector } from '../../../../client/common/terminal/shellDetectors/settingsShellDetector'; +import { TerminalNameShellDetector } from '../../../../client/common/terminal/shellDetectors/terminalNameShellDetector'; +import { UserEnvironmentShellDetector } from '../../../../client/common/terminal/shellDetectors/userEnvironmentShellDetector'; +import { VSCEnvironmentShellDetector } from '../../../../client/common/terminal/shellDetectors/vscEnvironmentShellDetector'; +import { ShellIdentificationTelemetry, TerminalShellType } from '../../../../client/common/terminal/types'; +import { getNamesAndValues } from '../../../../client/common/utils/enum'; +import { OSType } from '../../../../client/common/utils/platform'; + +suite('Shell Detectors', () => { + let platformService: IPlatformService; + let currentProcess: CurrentProcess; + let workspaceService: WorkspaceService; + let appEnv: ApplicationEnvironment; + + // Dummy data for testing. + const shellPathsAndIdentification = new Map<string, TerminalShellType>(); + shellPathsAndIdentification.set('c:\\windows\\system32\\cmd.exe', TerminalShellType.commandPrompt); + shellPathsAndIdentification.set('c:\\windows\\system32\\bash.exe', TerminalShellType.bash); + shellPathsAndIdentification.set('c:\\windows\\system32\\wsl.exe', TerminalShellType.wsl); + shellPathsAndIdentification.set('c:\\windows\\system32\\gitbash.exe', TerminalShellType.gitbash); + shellPathsAndIdentification.set('/usr/bin/bash', TerminalShellType.bash); + shellPathsAndIdentification.set('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); + shellPathsAndIdentification.set('c:\\windows\\system32\\shell.exe', TerminalShellType.other); + shellPathsAndIdentification.set('/usr/bin/shell', TerminalShellType.other); + shellPathsAndIdentification.set('/usr/bin/csh', TerminalShellType.cshell); + shellPathsAndIdentification.set('/usr/bin/tcsh', TerminalShellType.tcshell); + shellPathsAndIdentification.set('/usr/bin/xonsh', TerminalShellType.xonsh); + shellPathsAndIdentification.set('/usr/bin/xonshx', TerminalShellType.other); + + let telemetryProperties: ShellIdentificationTelemetry; + + setup(() => { + telemetryProperties = { + failed: true, + shellIdentificationSource: 'default', + terminalProvided: false, + hasCustomShell: undefined, + hasShellInEnv: undefined, + }; + platformService = mock(PlatformService); + workspaceService = mock(WorkspaceService); + currentProcess = mock(CurrentProcess); + appEnv = mock(ApplicationEnvironment); + }); + test('Test Priority of detectors', async () => { + expect(new TerminalNameShellDetector().priority).to.equal(4); + expect(new VSCEnvironmentShellDetector(instance(appEnv)).priority).to.equal(3); + expect(new SettingsShellDetector(instance(workspaceService), instance(platformService)).priority).to.equal(2); + expect(new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)).priority).to.equal( + 1, + ); + }); + test('Test identification of Terminal Shells (base class method)', async () => { + const shellDetector = new TerminalNameShellDetector(); + shellPathsAndIdentification.forEach((shellType, shellPath) => { + expect(shellDetector.identifyShellFromShellPath(shellPath)).to.equal( + shellType, + `Incorrect Shell Type for path '${shellPath}'`, + ); + }); + }); + test('Identify shell based on name of terminal', async () => { + const shellDetector = new TerminalNameShellDetector(); + shellPathsAndIdentification.forEach((shellType, shellPath) => { + expect(shellDetector.identify(telemetryProperties, { name: shellPath } as any)).to.equal( + shellType, + `Incorrect Shell Type for name '${shellPath}'`, + ); + }); + + expect(shellDetector.identify(telemetryProperties, undefined)).to.equal( + undefined, + 'Should be undefined when there is no temrinal', + ); + }); + test('Identify shell based on 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}'`, + ); + }); + + when(appEnv.shell).thenReturn(undefined as any); + expect(shellDetector.identify(telemetryProperties, undefined)).to.equal( + 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)); + shellPathsAndIdentification.forEach((shellType, shellPath) => { + // Assume the same paths are stored in user settings, we should still be able to identify the shell. + shellDetector.getTerminalShellPath = () => shellPath; + expect(shellDetector.identify(telemetryProperties, {} as any)).to.equal( + shellType, + `Incorrect Shell Type for path '${shellPath}'`, + ); + }); + }); + getNamesAndValues<OSType>(OSType).forEach((os) => { + test(`Get shell path from settings (OS ${os.name})`, async () => { + const shellPathInSettings = 'some value'; + const shellDetector = new SettingsShellDetector(instance(workspaceService), instance(platformService)); + const getStub = sinon.stub(); + const config = { get: getStub } as any; + getStub.returns(shellPathInSettings); + when(workspaceService.getConfiguration('terminal.integrated.shell')).thenReturn(config); + when(platformService.osType).thenReturn(os.value); + + const shellPath = shellDetector.getTerminalShellPath(); + + expect(shellPath).to.equal(os.value === OSType.Unknown ? '' : shellPathInSettings); + expect(getStub.callCount).to.equal(os.value === OSType.Unknown ? 0 : 1); + if (os.value !== OSType.Unknown) { + expect(getStub.args[0][0]).to.equal(os.name.toLowerCase()); + } + }); + }); + test('Identify shell based on user environment variables', async () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + shellPathsAndIdentification.forEach((shellType, shellPath) => { + // Assume the same paths are defined in user environment variables, we should still be able to identify the shell. + shellDetector.getDefaultPlatformShell = () => shellPath; + expect(shellDetector.identify(telemetryProperties, {} as any)).to.equal( + shellType, + `Incorrect Shell Type for path '${shellPath}'`, + ); + }); + }); + test('Default shell on Windows < 10 is cmd.exe', () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + when(platformService.osType).thenReturn(OSType.Windows); + when(platformService.osRelease).thenReturn('7'); + when(currentProcess.env).thenReturn({}); + + const shellPath = shellDetector.getDefaultPlatformShell(); + + expect(shellPath).to.equal('cmd.exe'); + }); + test('Default shell on Windows >= 10 32bit is powershell.exe', () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + when(platformService.osType).thenReturn(OSType.Windows); + when(platformService.osRelease).thenReturn('10'); + when(currentProcess.env).thenReturn({ windir: 'WindowsDir', PROCESSOR_ARCHITEW6432: '', comspec: 'hello.exe' }); + + const shellPath = shellDetector.getDefaultPlatformShell(); + + expect(shellPath).to.equal('WindowsDir\\Sysnative\\WindowsPowerShell\\v1.0\\powershell.exe'); + }); + test('Default shell on Windows >= 10 64bit is powershell.exe', () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + when(platformService.osType).thenReturn(OSType.Windows); + when(platformService.osRelease).thenReturn('10'); + when(currentProcess.env).thenReturn({ windir: 'WindowsDir', comspec: 'hello.exe' }); + + const shellPath = shellDetector.getDefaultPlatformShell(); + + expect(shellPath).to.equal('WindowsDir\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'); + }); + test('Default shell on Windows < 10 is what ever is defined in env.comspec', () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + when(platformService.osType).thenReturn(OSType.Windows); + when(platformService.osRelease).thenReturn('7'); + when(currentProcess.env).thenReturn({ comspec: 'hello.exe' }); + + const shellPath = shellDetector.getDefaultPlatformShell(); + + expect(shellPath).to.equal('hello.exe'); + }); + [OSType.OSX, OSType.Linux].forEach((osType) => { + test(`Default shell on ${osType} is /bin/bash`, () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + when(platformService.osType).thenReturn(OSType.OSX); + when(currentProcess.env).thenReturn({}); + + const shellPath = shellDetector.getDefaultPlatformShell(); + + expect(shellPath).to.equal('/bin/bash'); + }); + test(`Default shell on ${osType} is what ever is in env.SHELL`, () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + when(platformService.osType).thenReturn(OSType.OSX); + when(currentProcess.env).thenReturn({ SHELL: 'hello terminal.app' }); + + const shellPath = shellDetector.getDefaultPlatformShell(); + + expect(shellPath).to.equal('hello terminal.app'); + }); + test(`Default shell on ${osType} is what ever is /bin/bash if env.SHELL == /bin/false`, () => { + const shellDetector = new UserEnvironmentShellDetector(instance(currentProcess), instance(platformService)); + when(platformService.osType).thenReturn(OSType.OSX); + when(currentProcess.env).thenReturn({ SHELL: '/bin/false' }); + + const shellPath = shellDetector.getDefaultPlatformShell(); + + expect(shellPath).to.equal('/bin/bash'); + }); + }); +}); diff --git a/src/test/common/terminals/synchronousTerminalService.unit.test.ts b/src/test/common/terminals/synchronousTerminalService.unit.test.ts new file mode 100644 index 000000000000..4b6e77ec8095 --- /dev/null +++ b/src/test/common/terminals/synchronousTerminalService.unit.test.ts @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as path from 'path'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { CancellationTokenSource } from 'vscode'; +import { CancellationError } from '../../../client/common/cancellation'; +import { FileSystem } from '../../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { TerminalService } from '../../../client/common/terminal/service'; +import { SynchronousTerminalService } from '../../../client/common/terminal/syncTerminalService'; +import { createDeferredFrom } from '../../../client/common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { noop, sleep } from '../../core'; + +suite('Terminal Service (synchronous)', () => { + let service: SynchronousTerminalService; + let fs: IFileSystem; + let interpreterService: IInterpreterService; + let terminalService: TerminalService; + setup(() => { + fs = mock(FileSystem); + interpreterService = mock(InterpreterService); + terminalService = mock(TerminalService); + service = new SynchronousTerminalService(instance(fs), instance(interpreterService), instance(terminalService)); + }); + suite('Show, sendText and dispose should invoke corresponding methods in wrapped TerminalService', () => { + test('Show should invoke show in terminal', async () => { + when(terminalService.show(anything())).thenResolve(); + await service.show(); + verify(terminalService.show(undefined)).once(); + }); + test('Show should invoke show in terminal (without chaning focus)', async () => { + when(terminalService.show(anything())).thenResolve(); + await service.show(false); + verify(terminalService.show(false)).once(); + }); + test('Show should invoke show in terminal (without chaning focus)', async () => { + when(terminalService.show(anything())).thenResolve(); + await service.show(false); + verify(terminalService.show(false)).once(); + }); + test('Show should invoke show in terminal (without chaning focus)', async () => { + when(terminalService.show(anything())).thenResolve(); + await service.show(false); + verify(terminalService.show(false)).once(); + }); + test('Dispose should dipose the wrapped TerminalService', async () => { + service.dispose(); + verify(terminalService.dispose()).once(); + }); + test('sendText should invokeSendText in wrapped TerminalService', async () => { + when(terminalService.sendText('Blah')).thenResolve(); + await service.sendText('Blah'); + verify(terminalService.sendText('Blah')).once(); + }); + test('sendText should invokeSendText in wrapped TerminalService (errors should be bubbled up)', async () => { + when(terminalService.sendText('Blah')).thenReject(new Error('kaboom')); + const promise = service.sendText('Blah'); + + await assert.isRejected(promise, 'kaboom'); + verify(terminalService.sendText('Blah')).once(); + }); + }); + suite('sendCommand', () => { + const 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(); + await service.sendCommand('cmd', ['1', '2']); + verify(terminalService.sendCommand('cmd', deepEqual(['1', '2']))).once(); + }); + test('run sendCommand in terminalService should be cancelled', async () => { + const cancel = new CancellationTokenSource(); + const tmpFile = { filePath: 'tmp with spaces', dispose: noop }; + when(terminalService.sendCommand(anything(), anything())).thenResolve(); + when(interpreterService.getActiveInterpreter(undefined)).thenResolve(undefined); + when(fs.createTemporaryFile('.log')).thenResolve(tmpFile); + when(fs.readFile(anything())).thenResolve(''); + + // Send the necessary commands to the terminal. + const promise = service.sendCommand('cmd', ['1', '2'], cancel.token).catch((ex) => Promise.reject(ex)); + + const deferred = createDeferredFrom(promise); + // required to shutup node (we must handled exceptions). + deferred.promise.ignoreErrors(); + + // Should not have completed. + assert.isFalse(deferred.completed); + + // Wait for some time, and it should still not be completed + // Should complete only after command has executed successfully or been cancelled. + await sleep(500); + assert.isFalse(deferred.completed); + + // If cancelled, then throw cancellation error. + cancel.cancel(); + + await assert.isRejected(promise, new CancellationError().message); + verify(fs.createTemporaryFile('.log')).once(); + verify(fs.readFile(tmpFile.filePath)).atLeast(1); + verify( + terminalService.sendCommand( + 'python', + deepEqual([shellExecFile, 'cmd', '1', '2', tmpFile.filePath.fileToCommandArgumentForPythonExt()]), + ), + ).once(); + }).timeout(1_000); + test('run sendCommand in terminalService should complete when command completes', async () => { + const cancel = new CancellationTokenSource(); + const tmpFile = { filePath: 'tmp with spaces', dispose: noop }; + when(terminalService.sendCommand(anything(), anything())).thenResolve(); + when(interpreterService.getActiveInterpreter(undefined)).thenResolve(undefined); + when(fs.createTemporaryFile('.log')).thenResolve(tmpFile); + when(fs.readFile(anything())).thenResolve(''); + + // Send the necessary commands to the terminal. + const promise = service.sendCommand('cmd', ['1', '2'], cancel.token).catch((ex) => Promise.reject(ex)); + + const deferred = createDeferredFrom(promise); + // required to shutup node (we must handled exceptions). + deferred.promise.ignoreErrors(); + + // Should not have completed. + assert.isFalse(deferred.completed); + + // Wait for some time, and it should still not be completed + // Should complete only after command has executed successfully or been cancelled. + await sleep(500); + assert.isFalse(deferred.completed); + + // Write `END` into file, to trigger completion of the command. + when(fs.readFile(anything())).thenResolve('END'); + + await promise; + verify(fs.createTemporaryFile('.log')).once(); + verify(fs.readFile(tmpFile.filePath)).atLeast(1); + verify( + terminalService.sendCommand( + 'python', + deepEqual([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 72c8ed887481..6b6d41d552c3 100644 --- a/src/test/common/utils/async.unit.test.ts +++ b/src/test/common/utils/async.unit.test.ts @@ -4,46 +4,343 @@ '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 => { + test('Resolve', (done) => { const valueToSent = new Date().getTime(); const def = createDeferred<number>(); - def.promise.then(value => { - assert.equal(value, valueToSent); - assert.equal(def.resolved, true, 'resolved property value is not `true`'); - }).then(done).catch(done); + def.promise + .then((value) => { + 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 => { + test('Reject', (done) => { const errorToSend = new Error('Something'); const def = createDeferred<number>(); - def.promise.then(value => { - assert.fail(value, 'Error', 'Was expecting promise to get rejected, however it was resolved', ''); - done(); - }).catch(reason => { - assert.equal(reason, errorToSend, 'Error received is not the same'); - done(); - }).catch(done); - - assert.equal(def.resolved, false, 'Promise is resolved even when it should not be'); - assert.equal(def.rejected, false, 'Promise is rejected even when it should not be'); - assert.equal(def.completed, false, 'Promise is completed even when it should not be'); + def.promise + .then((value) => { + assert.fail(value, 'Error', 'Was expecting promise to get rejected, however it was resolved', ''); + done(); + }) + .catch((reason) => { + assert.strictEqual(reason, errorToSend, 'Error received is not the same'); + done(); + }) + .catch(done); + + 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 new file mode 100644 index 000000000000..01a11f4b4585 --- /dev/null +++ b/src/test/common/utils/cacheUtils.unit.test.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { InMemoryCache } from '../../../client/common/utils/cacheUtils'; + +suite('Common Utils - CacheUtils', () => { + suite('InMemory Cache', () => { + let clock: sinon.SinonFakeTimers; + setup(() => { + clock = sinon.useFakeTimers(); + }); + teardown(() => clock.restore()); + test('Cached item should exist', () => { + const cache = new InMemoryCache(5_000); + cache.data = 'Hello World'; + + assert.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.strictEqual(cache.data, 'Hello World'); + assert.isOk(cache.hasData); + + 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.strictEqual(cache.data, 'Hello World'); + assert.isTrue(cache.hasData); + + // Should not expire after 4.999s. + clock.tick(4_999); + + assert.strictEqual(cache.data, 'Hello World'); + assert.isTrue(cache.hasData); + + // Should expire after 5s (previous 4999ms + 1ms). + clock.tick(1); + + assert.strictEqual(cache.data, undefined); + assert.isFalse(cache.hasData); + }); + }); +}); diff --git a/src/test/common/utils/decorators.unit.test.ts b/src/test/common/utils/decorators.unit.test.ts new file mode 100644 index 000000000000..b1e86c4e2013 --- /dev/null +++ b/src/test/common/utils/decorators.unit.test.ts @@ -0,0 +1,330 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaiPromise from 'chai-as-promised'; +import { clearCache } from '../../../client/common/utils/cacheUtils'; +import { cache, makeDebounceAsyncDecorator, makeDebounceDecorator } from '../../../client/common/utils/decorators'; +import { sleep } from '../../core'; +use(chaiPromise.default); + +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. + + this.retries(3); + suite('Cache Decorator', () => { + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + + setup(() => { + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + }); + + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + clearCache(); + }); + class TestClass { + public invoked = false; + @cache(1000) + public async doSomething(a: number, b: number): Promise<number> { + this.invoked = true; + return a + b; + } + } + + test('Result should be cached for 1s', async () => { + const cls = new TestClass(); + expect(cls.invoked).to.equal(false, 'Wrong initialization value'); + await expect(cls.doSomething(1, 2)).to.eventually.equal(3); + expect(cls.invoked).to.equal(true, 'Should have been invoked'); + + // Reset and ensure it is not updated. + cls.invoked = false; + await expect(cls.doSomething(1, 2)).to.eventually.equal(3); + expect(cls.invoked).to.equal(false, 'Should not have been invoked'); + await expect(cls.doSomething(1, 2)).to.eventually.equal(3); + expect(cls.invoked).to.equal(false, 'Should not have been invoked'); + + // Cache should expire. + await sleep(2000); + + await expect(cls.doSomething(1, 2)).to.eventually.equal(3); + expect(cls.invoked).to.equal(true, 'Should have been invoked'); + // Reset and ensure it is not updated. + cls.invoked = false; + await expect(cls.doSomething(1, 2)).to.eventually.equal(3); + expect(cls.invoked).to.equal(false, 'Should not have been invoked'); + }).timeout(3000); + }); + + suite('Debounce', () => { + /* + * Time in milliseconds (from some arbitrary point in time for current process). + * Don't use new Date().getTime() to calculate differences in times. + * Similarly setTimeout doesn't always trigger at prescribed time (accuracy isn't guaranteed). + * This has an accuracy of around 2-20ms. + * However we're dealing with tests that need accuracy of 1ms. + * Use API that'll give us better accuracy when dealing with elapsed times. + */ + function getHighPrecisionTime(): number { + const currentTime = process.hrtime(); + // Convert seconds to ms and nanoseconds to ms. + return currentTime[0] * 1000 + currentTime[1] / 1000_000; + } + + /** + * setTimeout doesn't always trigger at prescribed time (accuracy isn't guaranteed). + * Allow a discrepancy of +-5%. + * Here's a simple test to prove this (this has been reported by others too): + * ```js + * // Execute the following around 100 times, you'll see at least one where elapsed time is < 100. + * const startTime = .... + * await new Promise(resolve = setTimeout(resolve, 100)) + * console.log(currentTime - startTijme) + * ``` + */ + function assertElapsedTimeWithinRange(actualDelay: number, expectedDelay: number) { + const difference = actualDelay - expectedDelay; + if (difference >= 0) { + return; + } + expect(Math.abs(difference)).to.be.lessThan( + expectedDelay * 0.05, + `Actual delay ${actualDelay}, expected delay ${expectedDelay}, not within 5% of accuracy`, + ); + } + + class Base { + public created: number; + public calls: string[]; + public timestamps: number[]; + constructor() { + this.created = getHighPrecisionTime(); + this.calls = []; + this.timestamps = []; + } + protected _addCall(funcname: string, timestamp?: number): void { + if (!timestamp) { + timestamp = getHighPrecisionTime(); + } + this.calls.push(funcname); + this.timestamps.push(timestamp); + } + } + async function waitForCalls(timestamps: number[], count: number, delay = 10, timeout = 1000) { + const steps = timeout / delay; + for (let i = 0; i < steps; i += 1) { + if (timestamps.length >= count) { + return; + } + await sleep(delay); + } + if (timestamps.length < count) { + throw Error(`timed out after ${timeout}ms`); + } + } + test('Debounce: one sync call', async () => { + const wait = 100; + + class One extends Base { + @makeDebounceDecorator(wait) + public run(): void { + this._addCall('run'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + one.run(); + await waitForCalls(one.timestamps, 1); + const delay = one.timestamps[0] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + }); + test('Debounce: one async call & no wait', async () => { + const wait = 100; + + class One extends Base { + @makeDebounceAsyncDecorator(wait) + public async run(): Promise<void> { + this._addCall('run'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + let errored = false; + one.run().catch(() => (errored = true)); + await waitForCalls(one.timestamps, 1); + const delay = one.timestamps[0] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + expect(errored).to.be.equal(false, "Exception raised when there shouldn't have been any"); + }); + test('Debounce: one async call', async () => { + const wait = 100; + + class One extends Base { + @makeDebounceAsyncDecorator(wait) + public async run(): Promise<void> { + this._addCall('run'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + await one.run(); + await waitForCalls(one.timestamps, 1); + const delay = one.timestamps[0] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + }); + test('Debounce: one async call and ensure exceptions are re-thrown', async () => { + const wait = 100; + + class One extends Base { + @makeDebounceAsyncDecorator(wait) + public async run(): Promise<void> { + this._addCall('run'); + throw new Error('Kaboom'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + let capturedEx: Error | undefined; + await one.run().catch((ex) => (capturedEx = ex)); + await waitForCalls(one.timestamps, 1); + const delay = one.timestamps[0] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + expect(capturedEx).to.not.be.equal(undefined, 'Exception not re-thrown'); + }); + test('Debounce: multiple async calls', async () => { + const wait = 100; + + class One extends Base { + @makeDebounceAsyncDecorator(wait) + public async run(): Promise<void> { + this._addCall('run'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + let errored = false; + one.run().catch(() => (errored = true)); + one.run().catch(() => (errored = true)); + one.run().catch(() => (errored = true)); + one.run().catch(() => (errored = true)); + await waitForCalls(one.timestamps, 1); + const delay = one.timestamps[0] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + expect(errored).to.be.equal(false, "Exception raised when there shouldn't have been any"); + }); + test('Debounce: multiple async calls when awaiting on all', async function () { + const wait = 100; + + class One extends Base { + @makeDebounceAsyncDecorator(wait) + public async run(): Promise<void> { + this._addCall('run'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + await Promise.all([one.run(), one.run(), one.run(), one.run()]); + await waitForCalls(one.timestamps, 1); + const delay = one.timestamps[0] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + }); + test('Debounce: multiple async calls & wait on some', async () => { + const wait = 100; + + class One extends Base { + @makeDebounceAsyncDecorator(wait) + public async run(): Promise<void> { + this._addCall('run'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + let errored = false; + one.run().catch(() => (errored = true)); + await one.run(); + one.run().catch(() => (errored = true)); + one.run().catch(() => (errored = true)); + await waitForCalls(one.timestamps, 2); + const delay = one.timestamps[1] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run', 'run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + expect(errored).to.be.equal(false, "Exception raised when there shouldn't have been any"); + }); + test('Debounce: multiple calls grouped', async () => { + const wait = 100; + + class One extends Base { + @makeDebounceDecorator(wait) + public run(): void { + this._addCall('run'); + } + } + const one = new One(); + + const start = getHighPrecisionTime(); + one.run(); + one.run(); + one.run(); + await waitForCalls(one.timestamps, 1); + const delay = one.timestamps[0] - start; + + assertElapsedTimeWithinRange(delay, wait); + expect(one.calls).to.deep.equal(['run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + }); + test('Debounce: multiple calls spread', async () => { + const wait = 100; + + class One extends Base { + @makeDebounceDecorator(wait) + public run(): void { + this._addCall('run'); + } + } + const one = new One(); + + one.run(); + await sleep(wait); + one.run(); + await waitForCalls(one.timestamps, 2); + + expect(one.calls).to.deep.equal(['run', 'run']); + expect(one.timestamps).to.have.lengthOf(one.calls.length); + }); + }); +}); diff --git a/src/test/common/utils/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/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 new file mode 100644 index 000000000000..8b2214de11ba --- /dev/null +++ b/src/test/common/utils/regexp.unit.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; + +import { verboseRegExp } from '../../../client/common/utils/regexp'; + +suite('Utils for regular expressions - verboseRegExp()', () => { + test('whitespace removed in multiline pattern (example of typical usage)', () => { + const regex = verboseRegExp(` + ^ + (?: + spam \\b .* + ) | + (?: + eggs \\b .* + ) + $ + `); + + expect(regex.source).to.equal('^(?:spam\\b.*)|(?:eggs\\b.*)$', 'mismatch'); + }); + + const whitespaceTests = [ + ['spam eggs', 'spameggs'], + [ + `spam + eggs`, + 'spameggs', + ], + // empty + [' ', '(?:)'], + [ + ` + `, + '(?:)', + ], + ]; + for (const [pat, expected] of whitespaceTests) { + test(`whitespace removed ("${pat}")`, () => { + const regex = verboseRegExp(pat); + + expect(regex.source).to.equal(expected, 'mismatch'); + }); + } + + const noopPatterns = ['^(?:spam\\b.*)$', 'spam', '^spam$', 'spam$', '^spam']; + for (const pat of noopPatterns) { + test(`pattern not changed ("${pat}")`, () => { + const regex = verboseRegExp(pat); + + expect(regex.source).to.equal(pat, 'mismatch'); + }); + } + + const emptyPatterns = [ + '', + ` + `, + ' ', + ]; + for (const pat of emptyPatterns) { + test(`no pattern ("${pat}")`, () => { + const regex = verboseRegExp(pat); + + expect(regex.source).to.equal('(?:)', 'mismatch'); + }); + } +}); diff --git a/src/test/common/utils/string.unit.test.ts b/src/test/common/utils/string.unit.test.ts deleted file mode 100644 index da0347330cef..000000000000 --- a/src/test/common/utils/string.unit.test.ts +++ /dev/null @@ -1,25 +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 } from 'chai'; -import { splitParent } from '../../../client/common/utils/string'; - -suite('splitParent()', () => { - test('valid values', async () => { - const tests: [string, [string, string]][] = [ - ['x.y', ['x', 'y']], - ['x', ['', 'x']], - ['x.y.z', ['x.y', 'z']], - ['', ['', '']] - ]; - for (const [raw, expected] of tests) { - const result = splitParent(raw); - - expect(result).to.deep.equal(expected); - } - }); -}); diff --git a/src/test/common/utils/text.unit.test.ts b/src/test/common/utils/text.unit.test.ts index c8d0c10d8fd4..7e7a22896e9a 100644 --- a/src/test/common/utils/text.unit.test.ts +++ b/src/test/common/utils/text.unit.test.ts @@ -3,59 +3,30 @@ '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 () => { const tests: [string, Range][] = [ - ['1:5-3:5', new Range( - new Position(1, 5), - new Position(3, 5) - )], - ['1:5-3:3', new Range( - new Position(1, 5), - new Position(3, 3) - )], - ['1:3-3:5', new Range( - new Position(1, 3), - new Position(3, 5) - )], - ['1-3:5', new Range( - new Position(1, 0), - new Position(3, 5) - )], - ['1-3', new Range( - new Position(1, 0), - new Position(3, 0) - )], - ['1-1', new Range( - new Position(1, 0), - new Position(1, 0) - )], - ['1', new Range( - new Position(1, 0), - new Position(1, 0) - )], - ['1:3-', new Range( - new Position(1, 3), - new Position(0, 0) // ??? - )], - ['1:3', new Range( - new Position(1, 3), - new Position(1, 3) - )], - ['', new Range( - new Position(0, 0), - new Position(0, 0) - )], - ['3-1', new Range( - new Position(3, 0), - new Position(1, 0) - )] + ['1:5-3:5', new Range(new Position(1, 5), new Position(3, 5))], + ['1:5-3:3', new Range(new Position(1, 5), new Position(3, 3))], + ['1:3-3:5', new Range(new Position(1, 3), new Position(3, 5))], + ['1-3:5', new Range(new Position(1, 0), new Position(3, 5))], + ['1-3', new Range(new Position(1, 0), new Position(3, 0))], + ['1-1', new Range(new Position(1, 0), new Position(1, 0))], + ['1', new Range(new Position(1, 0), new Position(1, 0))], + [ + '1:3-', + new Range( + new Position(1, 3), + new Position(0, 0), // ??? + ), + ], + ['1:3', new Range(new Position(1, 3), new Position(1, 3))], + ['', new Range(new Position(0, 0), new Position(0, 0))], + ['3-1', new Range(new Position(3, 0), new Position(1, 0))], ]; for (const [raw, expected] of tests) { const result = parseRange(raw); @@ -64,12 +35,7 @@ suite('parseRange()', () => { } }); test('valid numbers', async () => { - const tests: [number, Range][] = [ - [1, new Range( - new Position(1, 0), - new Position(1, 0) - )] - ]; + const tests: [number, Range][] = [[1, new Range(new Position(1, 0), new Position(1, 0))]]; for (const [raw, expected] of tests) { const result = parseRange(raw); @@ -96,7 +62,7 @@ suite('parseRange()', () => { 'a-b', 'a', 'a:1', - 'a:b' + 'a:b', ]; for (const raw of tests) { expect(() => parseRange(raw)).to.throw(); @@ -109,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); @@ -118,9 +84,7 @@ suite('parsePosition()', () => { } }); test('valid numbers', async () => { - const tests: [number, Position][] = [ - [1, new Position(1, 0)] - ]; + const tests: [number, Position][] = [[1, new Position(1, 0)]]; for (const [raw, expected] of tests) { const result = parsePosition(raw); @@ -128,13 +92,58 @@ suite('parsePosition()', () => { } }); test('bad strings', async () => { - const tests: string[] = [ - '1:2:3', - '1:a', - 'a' - ]; + const tests: string[] = ['1:2:3', '1:a', 'a']; for (const raw of tests) { expect(() => parsePosition(raw)).to.throw(); } }); }); + +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 index d43def54e1ec..3541b9b82926 100644 --- a/src/test/common/utils/version.unit.test.ts +++ b/src/test/common/utils/version.unit.test.ts @@ -1,46 +1,348 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; +import * as assert from 'assert'; -// tslint:disable: no-any +import { + getVersionString, + isVersionInfoEmpty, + normalizeVersionInfo, + ParseResult, + parseVersionInfo, + validateVersionInfo, + VersionInfo, +} from '../../../client/common/utils/version'; -import * as assert from 'assert'; -import { parsePythonVersion } from '../../../client/common/utils/version'; - -suite('Version Utils', () => { - test('Must convert undefined if empty strinfg', async () => { - 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'); +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 7f0cb344d925..3ba073d71474 100644 --- a/src/test/common/variables/envVarsProvider.multiroot.test.ts +++ b/src/test/common/variables/envVarsProvider.multiroot.test.ts @@ -3,49 +3,65 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import * as fs from 'fs-extra'; -import { EOL } from 'os'; import * as path from 'path'; +import { anything } from 'ts-mockito'; import { ConfigurationTarget, Disposable, Uri, workspace } from 'vscode'; -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 { WorkspaceService } from '../../../client/common/application/workspace'; import { PlatformService } from '../../../client/common/platform/platformService'; +import { IFileSystem } from '../../../client/common/platform/types'; import { IDisposableRegistry, IPathUtils } from '../../../client/common/types'; -import { createDeferred } from '../../../client/common/utils/async'; +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 { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterAutoSelectionService } from '../../../client/interpreter/autoSelection/types'; -import { clearPythonPathInWorkspaceFolder, updateSetting } from '../../common'; +import { clearPythonPathInWorkspaceFolder, isOs, OSType, updateSetting } from '../../common'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../../initialize'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; import { MockProcess } from '../../mocks/process'; -import { UnitTestIocContainer } from '../../unittests/serviceRegistry'; +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 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(); + 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); @@ -59,12 +75,19 @@ suite('Multiroot Environment Variables Provider', () => { function getVariablesProvider(mockVariables: EnvironmentVariables = { ...process.env }) { const pathUtils = ioc.serviceContainer.get<IPathUtils>(IPathUtils); + const fs = ioc.serviceContainer.get<IFileSystem>(IFileSystem); const mockProcess = new MockProcess(mockVariables); - const variablesService = new EnvironmentVariablesService(pathUtils); + 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); - return new EnvironmentVariablesProvider(variablesService, disposables, new PlatformService(), cfgService, mockProcess); + const workspaceService = new WorkspaceService(); + return new EnvironmentVariablesProvider( + variablesService, + disposables, + new PlatformService(), + workspaceService, + mockProcess, + ); } test('Custom variables should not be undefined without an env file', async () => { @@ -75,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) { @@ -90,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) { @@ -103,13 +124,17 @@ suite('Multiroot Environment Variables Provider', () => { expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); - Object.keys(processVariables).forEach(variable => { - expect(vars).to.have.property(variable, processVariables[variable], 'Value of the variable is incorrect'); + Object.keys(processVariables).forEach((variable) => { + expect(vars).to.have.property(variable); + // On CI, it was seen that processVariable[variable] can contain spaces at the end, which causes tests to fail. So trim the strings before comparing. + expect(vars[variable]?.trim()).to.equal( + processVariables[variable]?.trim(), + 'Value of the variable is incorrect', + ); }); }); test('Variables from file should take precedence over variables in process', async () => { - // tslint:disable-next-line:no-invalid-template-strings await updateSetting('envFile', '${workspaceRoot}/.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); const processVariables = { ...process.env }; if (processVariables.PYTHONPATH) { @@ -127,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'; @@ -141,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'; @@ -156,8 +179,13 @@ suite('Multiroot Environment Variables Provider', () => { expect(vars).to.have.property(pathVariableName, processVariables[pathVariableName], 'PATH value is invalid'); }); - test('PATH from process variables should be included in in variables returned', async () => { - // tslint:disable-next-line:no-invalid-template-strings + test('PATH from process variables should be included in in variables returned', async function () { + // this test is flaky on windows (likely the value of the path property + // has incorrect path separator chars). Tracked by GH #4756 + if (isOs(OSType.Windows)) { + this.skip(); + } + await updateSetting('envFile', '${workspaceRoot}/.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); const processVariables = { ...process.env }; processVariables.PYTHONPATH = '/usr/one/TWO'; @@ -172,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'; @@ -190,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) { @@ -212,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'; @@ -227,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) { @@ -250,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); @@ -268,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); @@ -279,8 +303,11 @@ suite('Multiroot Environment Variables Provider', () => { expect(vars).to.have.property(randomEnvVariable, '1234', 'Yikes process variable has leaked'); }); - test('Custom variables will be refreshed when settings points to a different env file', async () => { - // tslint:disable-next-line:no-invalid-template-strings + test('Custom variables will be refreshed when settings points to a different env file', async function () { + // https://github.com/microsoft/vscode-python/issues/12563 + + return this.skip(); + await updateSetting('envFile', '${workspaceRoot}/.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); const processVariables = { ...process.env }; if (processVariables.PYTHONPATH) { @@ -293,118 +320,15 @@ 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. - await new Promise(resolve => setTimeout(resolve, 5000)); + await new Promise((resolve) => setTimeout(resolve, 5000)); const newVars = await envProvider.getEnvironmentVariables(workspace4PyFile); expect(newVars).to.not.equal(undefined, 'Variables is is undefiend'); expect(newVars).to.have.property('X12345PYEXTUNITTESTVAR', '12345', 'X12345PYEXTUNITTESTVAR value is invalid'); expect(newVars).to.not.to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); }); - - test('Custom variables will be refreshed when .env file is created, modified and deleted', async function () { - // tslint:disable-next-line:no-invalid-this - this.timeout(20000); - const env3 = path.join(workspace4Path.fsPath, '.env3'); - const fileExists = await fs.pathExists(env3); - if (fileExists) { - await fs.remove(env3); - } - // tslint:disable-next-line:no-invalid-template-strings - await updateSetting('envFile', '${workspaceRoot}/.env3', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); - const processVariables = { ...process.env }; - if (processVariables.PYTHONPATH) { - delete processVariables.PYTHONPATH; - } - const envProvider = getVariablesProvider(processVariables); - const vars = envProvider.getEnvironmentVariables(workspace4PyFile); - await expect(vars).to.eventually.not.equal(undefined, 'Variables is is undefiend'); - - // Create env3. - const contents = fs.readFileSync(path.join(workspace4Path.fsPath, '.env2')); - fs.writeFileSync(env3, contents); - - // Wait for settings to get refreshed. - await new Promise(resolve => setTimeout(resolve, 5000)); - - const newVars = await envProvider.getEnvironmentVariables(workspace4PyFile); - expect(newVars).to.not.equal(undefined, 'Variables is is undefiend after creating'); - expect(newVars).to.have.property('X12345PYEXTUNITTESTVAR', '12345', 'X12345PYEXTUNITTESTVAR value is invalid after creating'); - expect(newVars).to.not.to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid after creating'); - - // Modify env3. - fs.writeFileSync(env3, `${contents}${EOL}X123456PYEXTUNITTESTVAR=123456`); - - // Wait for settings to get refreshed. - await new Promise(resolve => setTimeout(resolve, 5000)); - - const updatedVars = await envProvider.getEnvironmentVariables(workspace4PyFile); - expect(updatedVars).to.not.equal(undefined, 'Variables is is undefiend after modifying'); - expect(updatedVars).to.have.property('X12345PYEXTUNITTESTVAR', '12345', 'X12345PYEXTUNITTESTVAR value is invalid after modifying'); - expect(updatedVars).to.not.to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid after modifying'); - expect(updatedVars).to.have.property('X123456PYEXTUNITTESTVAR', '123456', 'X123456PYEXTUNITTESTVAR value is invalid after modifying'); - - // Now remove env3. - await fs.remove(env3); - - // Wait for settings to get refreshed. - await new Promise(resolve => setTimeout(resolve, 5000)); - - const varsAfterDeleting = await envProvider.getEnvironmentVariables(workspace4PyFile); - expect(varsAfterDeleting).to.not.equal(undefined, 'Variables is undefiend after deleting'); - }); - - test('Change event will be raised when when .env file is created, modified and deleted', async function () { - // tslint:disable-next-line:no-invalid-this - this.timeout(20000); - const env3 = path.join(workspace4Path.fsPath, '.env3'); - const fileExists = await fs.pathExists(env3); - if (fileExists) { - await fs.remove(env3); - } - // tslint:disable-next-line:no-invalid-template-strings - await updateSetting('envFile', '${workspaceRoot}/.env3', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); - const processVariables = { ...process.env }; - if (processVariables.PYTHONPATH) { - delete processVariables.PYTHONPATH; - } - const envProvider = getVariablesProvider(processVariables); - let eventRaisedPromise = createDeferred<boolean>(); - envProvider.onDidEnvironmentVariablesChange(() => eventRaisedPromise.resolve(true)); - const vars = envProvider.getEnvironmentVariables(workspace4PyFile); - await expect(vars).to.eventually.not.equal(undefined, 'Variables is is undefiend'); - - // Create env3. - const contents = fs.readFileSync(path.join(workspace4Path.fsPath, '.env2')); - fs.writeFileSync(env3, contents); - - // Wait for settings to get refreshed. - await new Promise(resolve => setTimeout(resolve, 5000)); - - let eventRaised = await eventRaisedPromise.promise; - expect(eventRaised).to.equal(true, 'Create notification not raised'); - - // Modify env3. - eventRaisedPromise = createDeferred<boolean>(); - fs.writeFileSync(env3, `${contents}${EOL}X123456PYEXTUNITTESTVAR=123456`); - - // Wait for settings to get refreshed. - await new Promise(resolve => setTimeout(resolve, 5000)); - - eventRaised = await eventRaisedPromise.promise; - expect(eventRaised).to.equal(true, 'Change notification not raised'); - - // Now remove env3. - eventRaisedPromise = createDeferred<boolean>(); - await fs.remove(env3); - - // Wait for settings to get refreshed. - await new Promise(resolve => setTimeout(resolve, 5000)); - - eventRaised = await eventRaisedPromise.promise; - expect(eventRaised).to.equal(true, 'Delete notification not raised'); - }); }); diff --git a/src/test/common/variables/envVarsService.functional.test.ts b/src/test/common/variables/envVarsService.functional.test.ts new file mode 100644 index 000000000000..3cf55eddbd45 --- /dev/null +++ b/src/test/common/variables/envVarsService.functional.test.ts @@ -0,0 +1,36 @@ +// 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 { FileSystem } from '../../../client/common/platform/fileSystem'; +import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { IPathUtils } from '../../../client/common/types'; +import { OSType } from '../../../client/common/utils/platform'; +import { EnvironmentVariablesService } from '../../../client/common/variables/environment'; +import { IEnvironmentVariablesService } from '../../../client/common/variables/types'; +import { getOSType } from '../../common'; + +use(chaiAsPromised.default); + +// Functional tests that run code using the VS Code API are found +// in envVarsService.test.ts. + +suite('Environment Variables Service', () => { + let pathUtils: IPathUtils; + let variablesService: IEnvironmentVariablesService; + setup(() => { + pathUtils = new PathUtils(getOSType() === OSType.Windows); + const fs = new FileSystem(); + variablesService = new EnvironmentVariablesService(pathUtils, fs); + }); + + suite('parseFile()', () => { + test('Custom variables should be undefined with no argument', async () => { + const vars = await variablesService.parseFile(undefined); + expect(vars).to.equal(undefined, 'Variables should be undefined'); + }); + }); +}); diff --git a/src/test/common/variables/envVarsService.test.ts b/src/test/common/variables/envVarsService.test.ts new file mode 100644 index 000000000000..c7151a8e33b9 --- /dev/null +++ b/src/test/common/variables/envVarsService.test.ts @@ -0,0 +1,91 @@ +// 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 { FileSystem } from '../../../client/common/platform/fileSystem'; +import { PathUtils } from '../../../client/common/platform/pathUtils'; +import { IPathUtils } from '../../../client/common/types'; +import { OSType } from '../../../client/common/utils/platform'; +import { EnvironmentVariablesService } from '../../../client/common/variables/environment'; +import { IEnvironmentVariablesService } from '../../../client/common/variables/types'; +import { getOSType } from '../../common'; + +use(chaiAsPromised.default); + +const envFilesFolderPath = path.join(__dirname, '..', '..', '..', '..', 'src', 'testMultiRootWkspc', 'workspace4'); + +// Functional tests that do not run code using the VS Code API are found +// in envVarsService.test.ts. + +suite('Environment Variables Service', () => { + let pathUtils: IPathUtils; + let variablesService: IEnvironmentVariablesService; + setup(() => { + pathUtils = new PathUtils(getOSType() === OSType.Windows); + const fs = new FileSystem(); + variablesService = new EnvironmentVariablesService(pathUtils, fs); + }); + + suite('parseFile()', () => { + test('Custom variables should be undefined with no argument', async () => { + const vars = await variablesService.parseFile(undefined); + expect(vars).to.equal(undefined, 'Variables should be undefined'); + }); + + test('Custom variables should be undefined with non-existent files', async () => { + const vars = await variablesService.parseFile(path.join(envFilesFolderPath, 'abcd')); + expect(vars).to.equal(undefined, 'Variables should be undefined'); + }); + + test('Custom variables should be undefined when folder name is passed instead of a file name', async () => { + const vars = await variablesService.parseFile(envFilesFolderPath); + expect(vars).to.equal(undefined, 'Variables should be undefined'); + }); + + test('Custom variables should be not undefined with a valid environment file', async () => { + const vars = await variablesService.parseFile(path.join(envFilesFolderPath, '.env')); + expect(vars).to.not.equal(undefined, 'Variables should be undefined'); + }); + + test('Custom variables should be parsed from env file', async () => { + const vars = await variablesService.parseFile(path.join(envFilesFolderPath, '.env')); + + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + }); + + test('PATH and PYTHONPATH from env file should be returned as is', async () => { + const vars = await variablesService.parseFile(path.join(envFilesFolderPath, '.env5')); + const expectedPythonPath = '/usr/one/three:/usr/one/four'; + const expectedPath = '/usr/x:/usr/y'; + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(Object.keys(vars!)).lengthOf(5, 'Incorrect number of variables'); + expect(vars).to.have.property('X', '1', 'X value is invalid'); + expect(vars).to.have.property('Y', '2', 'Y value is invalid'); + expect(vars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH value is invalid'); + expect(vars).to.have.property('PATH', expectedPath, 'PATH value is invalid'); + }); + + test('Simple variable substitution is supported', async () => { + const vars = await variablesService.parseFile(path.join(envFilesFolderPath, '.env6'), { + BINDIR: '/usr/bin', + }); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(3, 'Incorrect number of variables'); + expect(vars).to.have.property('REPO', '/home/user/git/foobar', 'value is invalid'); + expect(vars).to.have.property( + 'PYTHONPATH', + '/home/user/git/foobar/foo:/home/user/git/foobar/bar', + 'value is invalid', + ); + expect(vars).to.have.property('PYTHON', '/usr/bin/python3', 'value is invalid'); + }); + }); +}); diff --git a/src/test/common/variables/envVarsService.unit.test.ts b/src/test/common/variables/envVarsService.unit.test.ts index 764816ef7e19..3709d97b9f62 100644 --- a/src/test/common/variables/envVarsService.unit.test.ts +++ b/src/test/common/variables/envVarsService.unit.test.ts @@ -6,191 +6,663 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; -import { PathUtils } from '../../../client/common/platform/pathUtils'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem } from '../../../client/common/platform/types'; import { IPathUtils } from '../../../client/common/types'; -import { OSType } from '../../../client/common/utils/platform'; -import { EnvironmentVariablesService } from '../../../client/common/variables/environment'; -import { IEnvironmentVariablesService } from '../../../client/common/variables/types'; -import { getOSType } from '../../common'; +import { EnvironmentVariablesService, parseEnvFile } from '../../../client/common/variables/environment'; +import { getSearchPathEnvVarNames } from '../../../client/common/utils/exec'; -use(chaiAsPromised); +use(chaiAsPromised.default); -const envFilesFolderPath = path.join(__dirname, '..', '..', '..', '..', 'src', 'testMultiRootWkspc', 'workspace4'); +type PathVar = 'Path' | 'PATH'; +const PATHS = getSearchPathEnvVarNames(); -// tslint:disable-next-line:max-func-body-length suite('Environment Variables Service', () => { - let pathUtils: IPathUtils; - let variablesService: IEnvironmentVariablesService; + const filename = 'x/y/z/.env'; + const processEnvPath = getSearchPathEnvVarNames()[0]; + let pathUtils: TypeMoq.IMock<IPathUtils>; + let fs: TypeMoq.IMock<IFileSystem>; + let variablesService: EnvironmentVariablesService; setup(() => { - pathUtils = new PathUtils(getOSType() === OSType.Windows); - variablesService = new EnvironmentVariablesService(pathUtils); + pathUtils = TypeMoq.Mock.ofType<IPathUtils>(undefined, TypeMoq.MockBehavior.Strict); + fs = TypeMoq.Mock.ofType<IFileSystem>(undefined, TypeMoq.MockBehavior.Strict); + variablesService = new EnvironmentVariablesService( + // This is the only place that the mocks are used. + pathUtils.object, + fs.object, + ); }); + function verifyAll() { + pathUtils.verifyAll(); + fs.verifyAll(); + } + function setFile(fileName: string, text: string) { + fs.setup((f) => f.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. + } - test('Custom variables should be undefined with no argument', async () => { - const vars = await variablesService.parseFile(undefined); - expect(vars).to.equal(undefined, 'Variables should be undefined'); - }); + suite('parseFile()', () => { + test('Custom variables should be undefined with no argument', async () => { + const vars = await variablesService.parseFile(undefined); - test('Custom variables should be undefined with non-existent files', async () => { - const vars = await variablesService.parseFile(path.join(envFilesFolderPath, 'abcd')); - expect(vars).to.equal(undefined, 'Variables should be undefined'); - }); + expect(vars).to.equal(undefined, 'Variables should be undefined'); + verifyAll(); + }); - test('Custom variables should be undefined when folder name is passed instead of a file name', async () => { - const vars = await variablesService.parseFile(envFilesFolderPath); - expect(vars).to.equal(undefined, 'Variables should be undefined'); - }); + test('Custom variables should be undefined with non-existent files', async () => { + fs.setup((f) => f.pathExists(filename)) // Handle the specific file. + .returns(() => Promise.resolve(false)); // The file is missing. - test('Custom variables should be not undefined with a valid environment file', async () => { - const vars = await variablesService.parseFile(path.join(envFilesFolderPath, '.env')); - expect(vars).to.not.equal(undefined, 'Variables should be undefined'); - }); + const vars = await variablesService.parseFile(filename); - test('Custom variables should be parsed from env file', async () => { - const vars = await variablesService.parseFile(path.join(envFilesFolderPath, '.env')); + expect(vars).to.equal(undefined, 'Variables should be undefined'); + verifyAll(); + }); - expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); - expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); - expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); - expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); - }); + test('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.pathExists(dirname)) // Handle the specific "file". + .returns(() => Promise.resolve(false)); // It isn't a "regular" file. - test('PATH and PYTHONPATH from env file should be returned as is', async () => { - const vars = await variablesService.parseFile(path.join(envFilesFolderPath, '.env5')); - const expectedPythonPath = '/usr/one/three:/usr/one/four'; - const expectedPath = '/usr/x:/usr/y'; - expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); - expect(Object.keys(vars!)).lengthOf(5, 'Incorrect number of variables'); - expect(vars).to.have.property('X', '1', 'X value is invalid'); - expect(vars).to.have.property('Y', '2', 'Y value is invalid'); - expect(vars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH value is invalid'); - expect(vars).to.have.property('PATH', expectedPath, 'PATH value is invalid'); - }); + const vars = await variablesService.parseFile(dirname); - test('Ensure variables are merged', async () => { - const vars1 = { ONE: '1', TWO: 'TWO' }; - const vars2 = { ONE: 'ONE', THREE: '3' }; - variablesService.mergeVariables(vars1, vars2); - expect(Object.keys(vars1)).lengthOf(2, 'Source variables modified'); - expect(Object.keys(vars2)).lengthOf(3, 'Variables not merged'); - expect(vars2).to.have.property('ONE', 'ONE', 'Variable overwritten'); - expect(vars2).to.have.property('TWO', 'TWO', 'Incorrect value'); - expect(vars2).to.have.property('THREE', '3', 'Variable not merged'); - }); + expect(vars).to.equal(undefined, 'Variables should be undefined'); + verifyAll(); + }); - test('Ensure path variabnles variables are not merged into target', async () => { - const pathVariable = pathUtils.getPathVariableName(); - const vars1 = { ONE: '1', TWO: 'TWO', PYTHONPATH: 'PYTHONPATH' }; - vars1[pathVariable] = 'PATH'; - const vars2 = { ONE: 'ONE', THREE: '3' }; - variablesService.mergeVariables(vars1, vars2); - expect(Object.keys(vars1)).lengthOf(4, 'Source variables modified'); - expect(Object.keys(vars2)).lengthOf(3, 'Variables not merged'); - expect(vars2).to.have.property('ONE', 'ONE', 'Variable overwritten'); - expect(vars2).to.have.property('TWO', 'TWO', 'Incorrect value'); - expect(vars2).to.have.property('THREE', '3', 'Variable not merged'); - }); + test('Custom variables should be not undefined with a valid environment file', async () => { + setFile(filename, '...'); - test('Ensure path variabnles variables in target are left untouched', async () => { - const pathVariable = pathUtils.getPathVariableName(); - const vars1 = { ONE: '1', TWO: 'TWO' }; - const vars2 = { ONE: 'ONE', THREE: '3', PYTHONPATH: 'PYTHONPATH' }; - vars2[pathVariable] = 'PATH'; - variablesService.mergeVariables(vars1, vars2); - expect(Object.keys(vars1)).lengthOf(2, 'Source variables modified'); - expect(Object.keys(vars2)).lengthOf(5, 'Variables not merged'); - expect(vars2).to.have.property('ONE', 'ONE', 'Variable overwritten'); - expect(vars2).to.have.property('TWO', 'TWO', 'Incorrect value'); - expect(vars2).to.have.property('THREE', '3', 'Variable not merged'); - expect(vars2).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); - expect(vars2).to.have.property(pathVariable, 'PATH', 'Incorrect value'); - }); + const vars = await variablesService.parseFile(filename); - test('Ensure appending PATH has no effect if an undefined value or empty string is provided and PATH does not exist in vars object', async () => { - const vars = { ONE: '1' }; - variablesService.appendPath(vars); - expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.not.equal(undefined, 'Variables should be undefined'); + verifyAll(); + }); - variablesService.appendPath(vars, ''); - expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + test('Custom variables should be parsed from env file', async () => { + // src/testMultiRootWkspc/workspace4/.env + setFile( + filename, + ` +X1234PYEXTUNITTESTVAR=1234 +PYTHONPATH=../workspace5 + `, + ); - variablesService.appendPath(vars, ' ', ''); - expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - }); + const vars = await variablesService.parseFile(filename); + + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + verifyAll(); + }); + + test('PATH and PYTHONPATH from env file should be returned as is', async () => { + const expectedPythonPath = '/usr/one/three:/usr/one/four'; + const expectedPath = '/usr/x:/usr/y'; + // src/testMultiRootWkspc/workspace4/.env + setFile( + filename, + ` +X=1 +Y=2 +PYTHONPATH=/usr/one/three:/usr/one/four +# Unix PATH variable +PATH=/usr/x:/usr/y +# Windows Path variable +Path=/usr/x:/usr/y + `, + ); - test('Ensure appending PYTHONPATH has no effect if an undefined value or empty string is provided and PYTHONPATH does not exist in vars object', async () => { - const vars = { ONE: '1' }; - variablesService.appendPythonPath(vars); - expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + const vars = await variablesService.parseFile(filename); - variablesService.appendPythonPath(vars, ''); - expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(Object.keys(vars!)).lengthOf(5, 'Incorrect number of variables'); + expect(vars).to.have.property('X', '1', 'X value is invalid'); + expect(vars).to.have.property('Y', '2', 'Y value is invalid'); + expect(vars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH value is invalid'); + expect(vars).to.have.property('PATH', expectedPath, 'PATH value is invalid'); + verifyAll(); + }); - variablesService.appendPythonPath(vars, ' ', ''); - expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + test('Simple variable substitution is supported', async () => { + // src/testMultiRootWkspc/workspace4/.env + setFile( + filename, + + '\ +REPO=/home/user/git/foobar\n\ +PYTHONPATH=${REPO}/foo:${REPO}/bar\n\ +PYTHON=${BINDIR}/python3\n\ + ', + ); + + const vars = await variablesService.parseFile(filename, { BINDIR: '/usr/bin' }); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(3, 'Incorrect number of variables'); + expect(vars).to.have.property('REPO', '/home/user/git/foobar', 'value is invalid'); + expect(vars).to.have.property( + 'PYTHONPATH', + '/home/user/git/foobar/foo:/home/user/git/foobar/bar', + 'value is invalid', + ); + expect(vars).to.have.property('PYTHON', '/usr/bin/python3', 'value is invalid'); + verifyAll(); + }); }); - test('Ensure appending PATH has no effect if an empty string is provided and path does not exist in vars object', async () => { - const pathVariable = pathUtils.getPathVariableName(); - const vars = { ONE: '1' }; - vars[pathVariable] = 'PATH'; - variablesService.appendPath(vars); - expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property(pathVariable, 'PATH', 'Incorrect value'); - - variablesService.appendPath(vars, ''); - expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property(pathVariable, 'PATH', 'Incorrect value'); - - variablesService.appendPath(vars, ' ', ''); - expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property(pathVariable, 'PATH', 'Incorrect value'); + PATHS.map((pathVariable) => { + suite(`mergeVariables() (path var: ${pathVariable})`, () => { + setup(() => { + pathUtils + .setup((pu) => pu.getPathVariableName()) // This always gets called. + .returns(() => pathVariable as PathVar); // Pretend we're on a specific platform. + }); + + test('Ensure variables are merged', async () => { + const vars1 = { ONE: '1', TWO: 'TWO' }; + const vars2 = { ONE: 'ONE', THREE: '3' }; + + variablesService.mergeVariables(vars1, vars2); + + expect(Object.keys(vars1)).lengthOf(2, 'Source variables modified'); + expect(Object.keys(vars2)).lengthOf(3, 'Variables not merged'); + expect(vars2).to.have.property('ONE', 'ONE', 'Variable overwritten'); + expect(vars2).to.have.property('TWO', 'TWO', 'Incorrect value'); + expect(vars2).to.have.property('THREE', '3', 'Variable not merged'); + verifyAll(); + }); + + test('Ensure path variabnles variables are not merged into target', async () => { + const vars1 = { ONE: '1', TWO: 'TWO', PYTHONPATH: 'PYTHONPATH' }; + + (vars1 as any)[pathVariable] = 'PATH'; + const vars2 = { ONE: 'ONE', THREE: '3' }; + + variablesService.mergeVariables(vars1, vars2); + + expect(Object.keys(vars1)).lengthOf(4, 'Source variables modified'); + expect(Object.keys(vars2)).lengthOf(3, 'Variables not merged'); + expect(vars2).to.have.property('ONE', 'ONE', 'Variable overwritten'); + expect(vars2).to.have.property('TWO', 'TWO', 'Incorrect value'); + expect(vars2).to.have.property('THREE', '3', 'Variable not merged'); + verifyAll(); + }); + + test('Ensure path variables in target are left untouched', async () => { + const vars1 = { ONE: '1', TWO: 'TWO' }; + const vars2 = { ONE: 'ONE', THREE: '3', PYTHONPATH: 'PYTHONPATH' }; + + (vars2 as any)[pathVariable] = 'PATH'; + + variablesService.mergeVariables(vars1, vars2); + + expect(Object.keys(vars1)).lengthOf(2, 'Source variables modified'); + expect(Object.keys(vars2)).lengthOf(5, 'Variables not merged'); + expect(vars2).to.have.property('ONE', 'ONE', 'Variable overwritten'); + expect(vars2).to.have.property('TWO', 'TWO', 'Incorrect value'); + expect(vars2).to.have.property('THREE', '3', 'Variable not merged'); + expect(vars2).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); + expect(vars2).to.have.property(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(); + }); + }); }); - test('Ensure appending PYTHONPATH has no effect if an empty string is provided and PYTHONPATH does not exist in vars object', async () => { - const vars = { ONE: '1', PYTHONPATH: 'PYTHONPATH' }; - variablesService.appendPythonPath(vars); - expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); - - variablesService.appendPythonPath(vars, ''); - expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); - - variablesService.appendPythonPath(vars, ' ', ''); - expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); + PATHS.map((pathVariable) => { + suite(`appendPath() (path var: ${pathVariable})`, () => { + setup(() => { + pathUtils + .setup((pu) => pu.getPathVariableName()) // This always gets called. + .returns(() => pathVariable as PathVar); // Pretend we're on a specific platform. + }); + + test('Ensure appending PATH has no effect if an undefined value or empty string is provided and PATH does not exist in vars object', async () => { + const vars = { ONE: '1' }; + + variablesService.appendPath(vars); + expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + + variablesService.appendPath(vars, ''); + expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + + variablesService.appendPath(vars, ' ', ''); + expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + + verifyAll(); + }); + + test(`Ensure appending PATH has no effect if an empty string is provided and path does not exist in vars object (${pathVariable})`, async () => { + const vars = { ONE: '1' }; + + (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(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(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(processEnvPath, 'PATH', 'Incorrect value'); + + verifyAll(); + }); + + test(`Ensure PATH is appeneded (${pathVariable})`, async () => { + const vars = { ONE: '1' }; + + (vars as any)[pathVariable] = 'PATH'; + const pathToAppend = `/usr/one${path.delimiter}/usr/three`; + + variablesService.appendPath(vars, pathToAppend); + + expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.have.property( + processEnvPath, + `PATH${path.delimiter}${pathToAppend}`, + 'Incorrect value', + ); + verifyAll(); + }); + }); }); - test('Ensure PATH is appeneded', async () => { - const pathVariable = pathUtils.getPathVariableName(); - const vars = { ONE: '1' }; - vars[pathVariable] = 'PATH'; - const pathToAppend = `/usr/one${path.delimiter}/usr/three`; - variablesService.appendPath(vars, pathToAppend); - expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property(pathVariable, `PATH${path.delimiter}${pathToAppend}`, 'Incorrect value'); + suite('appendPythonPath()', () => { + test('Ensure appending PYTHONPATH has no effect if an undefined value or empty string is provided and PYTHONPATH does not exist in vars object', async () => { + const vars = { ONE: '1' }; + + variablesService.appendPythonPath(vars); + expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + + variablesService.appendPythonPath(vars, ''); + expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + + variablesService.appendPythonPath(vars, ' ', ''); + expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + + verifyAll(); + }); + + test('Ensure appending PYTHONPATH has no effect if an empty string is provided and PYTHONPATH does not exist in vars object', async () => { + const vars = { ONE: '1', PYTHONPATH: 'PYTHONPATH' }; + + variablesService.appendPythonPath(vars); + expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); + + variablesService.appendPythonPath(vars, ''); + expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); + + variablesService.appendPythonPath(vars, ' ', ''); + expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); + + verifyAll(); + }); + + test('Ensure appending PYTHONPATH has no effect if an empty string is provided and PYTHONPATH does not exist in vars object', async () => { + const vars = { ONE: '1', PYTHONPATH: 'PYTHONPATH' }; + const pathToAppend = `/usr/one${path.delimiter}/usr/three`; + + variablesService.appendPythonPath(vars, pathToAppend); + + expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + expect(vars).to.have.property( + 'PYTHONPATH', + `PYTHONPATH${path.delimiter}${pathToAppend}`, + 'Incorrect value', + ); + verifyAll(); + }); }); +}); + +suite('Parsing Environment Variables Files', () => { + suite('parseEnvFile()', () => { + test('Custom variables should be parsed from env file', () => { + const vars = parseEnvFile(` +X1234PYEXTUNITTESTVAR=1234 +PYTHONPATH=../workspace5 + `); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + }); + + test('PATH and PYTHONPATH from env file should be returned as is', () => { + const vars = parseEnvFile(` +X=1 +Y=2 +PYTHONPATH=/usr/one/three:/usr/one/four +# Unix PATH variable +PATH=/usr/x:/usr/y +# Windows Path variable +Path=/usr/x:/usr/y + `); + + const expectedPythonPath = '/usr/one/three:/usr/one/four'; + const expectedPath = '/usr/x:/usr/y'; + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(5, 'Incorrect number of variables'); + expect(vars).to.have.property('X', '1', 'X value is invalid'); + expect(vars).to.have.property('Y', '2', 'Y value is invalid'); + expect(vars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH value is invalid'); + expect(vars).to.have.property('PATH', expectedPath, 'PATH value is invalid'); + }); + + test('Variable names must be alpha + alnum/underscore', () => { + const vars = parseEnvFile(` +SPAM=1234 +ham=5678 +Eggs=9012 +1bogus2=... +bogus 3=... +bogus.4=... +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(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', () => { + const vars = parseEnvFile(` +SPAM= + `); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '', 'value is invalid'); + }); + + test('Outer quotation marks are removed and cause newline substitution', () => { + const vars = parseEnvFile(` +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', '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', () => { + const vars = parseEnvFile(` +SPAM=1234 +HAM =5678 +EGGS= 9012 +FOO = 3456 + BAR=7890 + BAZ = ABCD +VAR1=EFGH ... +VAR2=IJKL +VAR3=' MNOP ' + `); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(9, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); + expect(vars).to.have.property('HAM', '5678', 'value is invalid'); + expect(vars).to.have.property('EGGS', '9012', 'value is invalid'); + expect(vars).to.have.property('FOO', '3456', 'value is invalid'); + expect(vars).to.have.property('BAR', '7890', 'value is invalid'); + expect(vars).to.have.property('BAZ', 'ABCD', 'value is invalid'); + expect(vars).to.have.property('VAR1', 'EFGH ...', 'value is invalid'); + expect(vars).to.have.property('VAR2', 'IJKL', 'value is invalid'); + expect(vars).to.have.property('VAR3', ' MNOP ', 'value is invalid'); + }); + + test('Blank lines are ignored', () => { + const vars = parseEnvFile(` + +SPAM=1234 + +HAM=5678 + + + `); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); + expect(vars).to.have.property('HAM', '5678', 'value is invalid'); + }); + + test('Comments are ignored', () => { + const vars = parseEnvFile(` +# step 1 +SPAM=1234 + # step 2 +HAM=5678 +#step 3 +EGGS=9012 # ... +# done + `); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(3, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); + expect(vars).to.have.property('HAM', '5678', 'value is invalid'); + expect(vars).to.have.property('EGGS', '9012 # ...', 'value is invalid'); + }); + + suite('variable substitution', () => { + test('Basic substitution syntax', () => { + const vars = parseEnvFile( + '\ +REPO=/home/user/git/foobar \n\ +PYTHONPATH=${REPO}/foo:${REPO}/bar \n\ + ', + ); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('REPO', '/home/user/git/foobar', 'value is invalid'); + expect(vars).to.have.property( + 'PYTHONPATH', + '/home/user/git/foobar/foo:/home/user/git/foobar/bar', + 'value is invalid', + ); + }); + + test('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', () => { + const vars = parseEnvFile('\ +SPAM=1234 \n\ +EGGS=$SPAM \n\ + '); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); + expect(vars).to.have.property('EGGS', '$SPAM', 'value is invalid'); + }); + + test('Nested substitution is not supported', () => { + const vars = parseEnvFile( + '\ +SPAM=EGGS \n\ +EGGS=??? \n\ +HAM1="-- ${${SPAM}} --"\n\ +abcEGGSxyz=!!! \n\ +HAM2="-- ${abc${SPAM}xyz} --"\n\ +HAM3="-- ${${SPAM} --"\n\ +HAM4="-- ${${SPAM}} ${EGGS} --"\n\ + ', + ); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(7, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', 'EGGS', 'value is invalid'); + expect(vars).to.have.property('EGGS', '???', 'value is invalid'); + expect(vars).to.have.property('HAM1', '-- ${${SPAM}} --', 'value is invalid'); + expect(vars).to.have.property('abcEGGSxyz', '!!!', 'value is invalid'); + expect(vars).to.have.property('HAM2', '-- ${abc${SPAM}xyz} --', 'value is invalid'); + expect(vars).to.have.property('HAM3', '-- ${${SPAM} --', 'value is invalid'); + expect(vars).to.have.property('HAM4', '-- ${${SPAM}} ${EGGS} --', 'value is invalid'); + }); + + test('Other bad substitution syntax', () => { + const vars = parseEnvFile( + '\ +SPAM=EGGS \n\ +EGGS=??? \n\ +HAM1=${} \n\ +HAM2=${ \n\ +HAM3=${SPAM+EGGS} \n\ +HAM4=$SPAM \n\ + ', + ); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(6, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', 'EGGS', 'value is invalid'); + expect(vars).to.have.property('EGGS', '???', 'value is invalid'); + expect(vars).to.have.property('HAM1', '${}', 'value is invalid'); + expect(vars).to.have.property('HAM2', '${', 'value is invalid'); + expect(vars).to.have.property('HAM3', '${SPAM+EGGS}', 'value is invalid'); + expect(vars).to.have.property('HAM4', '$SPAM', 'value is invalid'); + }); + + test('Recursive substitution is allowed', () => { + const vars = parseEnvFile( + '\ +REPO=/home/user/git/foobar \n\ +PYTHONPATH=${REPO}/foo \n\ +PYTHONPATH=${PYTHONPATH}:${REPO}/bar \n\ + ', + ); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('REPO', '/home/user/git/foobar', 'value is invalid'); + expect(vars).to.have.property( + 'PYTHONPATH', + '/home/user/git/foobar/foo:/home/user/git/foobar/bar', + 'value is invalid', + ); + }); + + test('"$" 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(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', () => { + const vars = parseEnvFile('\ +PYTHONPATH=${REPO}/foo:${REPO}/bar \n\ + ', { + REPO: '/home/user/git/foobar', + }); - test('Ensure appending PYTHONPATH has no effect if an empty string is provided and PYTHONPATH does not exist in vars object', async () => { - const vars = { ONE: '1', PYTHONPATH: 'PYTHONPATH' }; - const pathToAppend = `/usr/one${path.delimiter}/usr/three`; - variablesService.appendPythonPath(vars, pathToAppend); - expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property('PYTHONPATH', `PYTHONPATH${path.delimiter}${pathToAppend}`, 'Incorrect value'); + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(1, 'Incorrect number of variables'); + expect(vars).to.have.property( + 'PYTHONPATH', + '/home/user/git/foobar/foo:/home/user/git/foobar/bar', + 'value is invalid', + ); + }); + }); }); }); diff --git a/src/test/common/variables/environmentVariablesProvider.unit.test.ts b/src/test/common/variables/environmentVariablesProvider.unit.test.ts new file mode 100644 index 000000000000..bf02f5f867d7 --- /dev/null +++ b/src/test/common/variables/environmentVariablesProvider.unit.test.ts @@ -0,0 +1,385 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import * as typemoq from 'typemoq'; +import { ConfigurationChangeEvent, FileSystemWatcher, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { CurrentProcess } from '../../../client/common/process/currentProcess'; +import { ICurrentProcess } from '../../../client/common/types'; +import { sleep } from '../../../client/common/utils/async'; +import { EnvironmentVariablesService } from '../../../client/common/variables/environment'; +import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; +import { IEnvironmentVariablesService } from '../../../client/common/variables/types'; +import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; +import { noop } from '../../core'; + +suite('Multiroot Environment Variables Provider', () => { + let provider: EnvironmentVariablesProvider; + let envVarsService: IEnvironmentVariablesService; + let platform: IPlatformService; + let workspace: IWorkspaceService; + let currentProcess: ICurrentProcess; + let envFile: string; + + setup(() => { + envFile = ''; + envVarsService = mock(EnvironmentVariablesService); + platform = mock(PlatformService); + workspace = mock(WorkspaceService); + currentProcess = mock(CurrentProcess); + + 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(currentProcess), + ); + + sinon.stub(EnvFileTelemetry, 'sendFileCreationTelemetry').returns(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Event is fired when there are changes to settings', () => { + let affectedWorkspace: Uri | undefined; + const workspaceFolder1Uri = Uri.file('workspace1'); + const workspaceFolder2Uri = Uri.file('workspace2'); + + provider.trackedWorkspaceFolders.add(workspaceFolder1Uri.fsPath); + provider.trackedWorkspaceFolders.add(workspaceFolder2Uri.fsPath); + provider.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); + const changedEvent: ConfigurationChangeEvent = { + affectsConfiguration(setting: string, uri?: Uri) { + return setting === 'python.envFile' && uri!.fsPath === workspaceFolder1Uri.fsPath; + }, + }; + + provider.configurationChanged(changedEvent); + + assert.ok(affectedWorkspace); + assert.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; + }); + const changedEvent: ConfigurationChangeEvent = { + affectsConfiguration() { + return false; + }, + }; + + provider.configurationChanged(changedEvent); + + assert.strictEqual(affectedWorkspace, undefined); + }); + test('Event is not fired when workspace is not tracked', () => { + let affectedWorkspace: Uri | undefined; + provider.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); + const changedEvent: ConfigurationChangeEvent = { + affectsConfiguration() { + return true; + }, + }; + + provider.configurationChanged(changedEvent); + + 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'); + 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; + }) + .verifiable(typemoq.Times.once()); + when(workspace.createFileSystemWatcher(envFile)).thenReturn(fileSystemWatcher.object); + provider.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); + + provider.createFileWatcher(envFile, workspaceUri); + + fileSystemWatcher.verifyAll(); + assert.ok(onChangeHandler); + + onChangeHandler!(); + + assert.strictEqual(affectedWorkspace, workspaceUri); + }); + test(`Event is fired when the environment file is deleted ${workspaceTitle}`, () => { + let affectedWorkspace: Uri | undefined = Uri.file('dummy value'); + 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; + }) + .verifiable(typemoq.Times.once()); + when(workspace.createFileSystemWatcher(envFile)).thenReturn(fileSystemWatcher.object); + provider.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); + + provider.createFileWatcher(envFile, workspaceUri); + + fileSystemWatcher.verifyAll(); + assert.ok(onDeleted); + + onDeleted!(); + + assert.strictEqual(affectedWorkspace, workspaceUri); + }); + test(`Event is fired when the environment file is created ${workspaceTitle}`, () => { + let affectedWorkspace: Uri | undefined = Uri.file('dummy value'); + 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; + }) + .verifiable(typemoq.Times.once()); + when(workspace.createFileSystemWatcher(envFile)).thenReturn(fileSystemWatcher.object); + provider.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); + + provider.createFileWatcher(envFile, workspaceUri); + + fileSystemWatcher.verifyAll(); + assert.ok(onCreated); + + onCreated!(); + + assert.strictEqual(affectedWorkspace, workspaceUri); + }); + test(`File system watcher event handlers are added once ${workspaceTitle}`, () => { + envFile = path.join('a', 'b', 'env.file'); + const fileSystemWatcher = typemoq.Mock.ofType<FileSystemWatcher>(); + + fileSystemWatcher.setup((fs) => fs.onDidChange(typemoq.It.isAny())).verifiable(typemoq.Times.once()); + fileSystemWatcher.setup((fs) => fs.onDidCreate(typemoq.It.isAny())).verifiable(typemoq.Times.once()); + fileSystemWatcher.setup((fs) => fs.onDidDelete(typemoq.It.isAny())).verifiable(typemoq.Times.once()); + when(workspace.createFileSystemWatcher(envFile)).thenReturn(fileSystemWatcher.object); + + provider.createFileWatcher(envFile); + provider.createFileWatcher(envFile); + provider.createFileWatcher(envFile, workspaceUri); + provider.createFileWatcher(envFile, workspaceUri); + provider.createFileWatcher(envFile, workspaceUri); + + fileSystemWatcher.verifyAll(); + verify(workspace.createFileSystemWatcher(envFile)).once(); + }); + + test(`Getting environment variables (without an envfile, without PATH in current env, without PYTHONPATH in current env) & ${workspaceTitle}`, async () => { + 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(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); + when(envVarsService.parseFile(envFile, currentProcEnv)).thenResolve(undefined); + when(platform.pathVariableName).thenReturn('PATH'); + + const vars = await provider.getEnvironmentVariables(workspaceUri); + + verify(currentProcess.env).atLeast(1); + verify(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 () => { + 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(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); + 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(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 () => { + 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(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); + 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(envVarsService.parseFile(envFile, currentProcEnv)).atLeast(1); + verify(envVarsService.mergeVariables(deepEqual(currentProcEnv), deepEqual(envFileVars))).once(); + verify(envVarsService.appendPath(deepEqual(envFileVars), currentProcEnv.PATH)).once(); + verify(envVarsService.appendPythonPath(deepEqual(envFileVars), currentProcEnv.PYTHONPATH)).once(); + verify(platform.pathVariableName).atLeast(1); + assert.deepEqual(vars, envFileVars); + }); + + test(`Getting environment variables which are already cached does not reinvoke the method ${workspaceTitle}`, async () => { + 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(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); + when(envVarsService.parseFile(envFile, currentProcEnv)).thenResolve(undefined); + when(platform.pathVariableName).thenReturn('PATH'); + + const vars = await provider.getEnvironmentVariables(workspaceUri); + + assert.deepEqual(vars, {}); + + await provider.getEnvironmentVariables(workspaceUri); + + // Verify that the contents of `_getEnvironmentVariables()` method are only invoked once + verify(workspace.getConfiguration('python', anything())).once(); + assert.deepEqual(vars, {}); + }); + + test(`Cache result must be cleared when cache expires ${workspaceTitle}`, async () => { + 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(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); + when(envVarsService.parseFile(envFile, currentProcEnv)).thenResolve(undefined); + when(platform.pathVariableName).thenReturn('PATH'); + + provider = new EnvironmentVariablesProvider( + instance(envVarsService), + [], + instance(platform), + instance(workspace), + instance(currentProcess), + 100, + ); + const vars = await provider.getEnvironmentVariables(workspaceUri); + + assert.deepEqual(vars, {}); + + await sleep(110); + await provider.getEnvironmentVariables(workspaceUri); + + // Verify that the contents of `_getEnvironmentVariables()` method are invoked twice + verify(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 new file mode 100644 index 000000000000..541579c609f7 --- /dev/null +++ b/src/test/common/variables/serviceRegistry.unit.test.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { EnvironmentVariablesService } from '../../../client/common/variables/environment'; +import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; +import { registerTypes } from '../../../client/common/variables/serviceRegistry'; +import { IEnvironmentVariablesProvider, IEnvironmentVariablesService } from '../../../client/common/variables/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; +import { IServiceManager } from '../../../client/ioc/types'; + +suite('Common variables Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager)); + verify( + serviceManager.addSingleton<IEnvironmentVariablesService>( + IEnvironmentVariablesService, + EnvironmentVariablesService, + ), + ).once(); + verify( + serviceManager.addSingleton<IEnvironmentVariablesProvider>( + IEnvironmentVariablesProvider, + EnvironmentVariablesProvider, + ), + ).once(); + }); +}); diff --git a/src/test/configuration/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.unit.test.ts b/src/test/configuration/interpreterSelector.unit.test.ts deleted file mode 100644 index 7fcf12024f67..000000000000 --- a/src/test/configuration/interpreterSelector.unit.test.ts +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as assert from 'assert'; -import { SemVer } from 'semver'; -import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import { PathUtils } from '../../client/common/platform/pathUtils'; -import { IFileSystem } from '../../client/common/platform/types'; -import { IConfigurationService, IPythonSettings } from '../../client/common/types'; -import { Architecture } from '../../client/common/utils/platform'; -import { IInterpreterQuickPickItem, InterpreterSelector } from '../../client/interpreter/configuration/interpreterSelector'; -import { IInterpreterComparer, IPythonPathUpdaterServiceManager } from '../../client/interpreter/configuration/types'; -import { IInterpreterService, InterpreterType, IShebangCodeLensProvider, PythonInterpreter } from '../../client/interpreter/contracts'; - -const info: PythonInterpreter = { - architecture: Architecture.Unknown, - companyDisplayName: '', - displayName: '', - envName: '', - path: '', - type: InterpreterType.Unknown, - version: new SemVer('1.0.0-alpha'), - sysPrefix: '', - sysVersion: '' -}; - -class InterpreterQuickPickItem implements IInterpreterQuickPickItem { - public path: string; - public label: string; - public description!: string; - public detail?: string; - constructor(l: string, p: string) { - this.path = p; - this.label = l; - } -} - -// tslint:disable-next-line:max-func-body-length -suite('Interpreters - selector', () => { - let workspace: TypeMoq.IMock<IWorkspaceService>; - let appShell: TypeMoq.IMock<IApplicationShell>; - let interpreterService: TypeMoq.IMock<IInterpreterService>; - let documentManager: TypeMoq.IMock<IDocumentManager>; - let fileSystem: TypeMoq.IMock<IFileSystem>; - let commandManager: TypeMoq.IMock<ICommandManager>; - let comparer: TypeMoq.IMock<IInterpreterComparer>; - let pythonPathUpdater: TypeMoq.IMock<IPythonPathUpdaterServiceManager>; - let shebangProvider: TypeMoq.IMock<IShebangCodeLensProvider>; - let configurationService: TypeMoq.IMock<IConfigurationService>; - let pythonSettings: TypeMoq.IMock<IPythonSettings>; - - class TestInterpreterSelector extends InterpreterSelector { - // tslint:disable-next-line:no-unnecessary-override - public async suggestionToQuickPickItem(suggestion: PythonInterpreter, workspaceUri?: Uri): Promise<IInterpreterQuickPickItem> { - return super.suggestionToQuickPickItem(suggestion, workspaceUri); - } - // tslint:disable-next-line:no-unnecessary-override - public async setInterpreter() { - return super.setInterpreter(); - } - // tslint:disable-next-line:no-unnecessary-override - public async setShebangInterpreter() { - return super.setShebangInterpreter(); - } - } - setup(() => { - commandManager = TypeMoq.Mock.ofType<ICommandManager>(); - comparer = TypeMoq.Mock.ofType<IInterpreterComparer>(); - appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); - documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - pythonPathUpdater = TypeMoq.Mock.ofType<IPythonPathUpdaterServiceManager>(); - shebangProvider = TypeMoq.Mock.ofType<IShebangCodeLensProvider>(); - configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); - pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - - workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - fileSystem - .setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) - .returns((a: string, b: string) => a === b); - fileSystem - .setup(x => x.getRealPath(TypeMoq.It.isAnyString())) - .returns((a: string) => new Promise(resolve => resolve(a))); - configurationService - .setup(x => x.getSettings(TypeMoq.It.isAny())) - .returns(() => pythonSettings.object); - - comparer.setup(c => c.compare(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => 0); - }); - - [true, false].forEach(isWindows => { - test(`Suggestions (${isWindows ? 'Windows' : 'Non-Windows'})`, async () => { - const selector = new InterpreterSelector(interpreterService.object, workspace.object, - appShell.object, documentManager.object, new PathUtils(isWindows), - comparer.object, pythonPathUpdater.object, shebangProvider.object, - configurationService.object, commandManager.object); - - 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 }; }); - interpreterService - .setup(x => x.getInterpreters(TypeMoq.It.isAny())) - .returns(() => new Promise((resolve) => resolve(initial))); - - const actual = await selector.getSuggestions(); - - const expected: InterpreterQuickPickItem[] = [ - new InterpreterQuickPickItem('1', 'c:/path1/path1'), - new InterpreterQuickPickItem('2', 'c:/path1/path1'), - new InterpreterQuickPickItem('2', 'c:/path2/path2'), - new InterpreterQuickPickItem('2 (virtualenv)', 'c:/path2/path2'), - new InterpreterQuickPickItem('3', 'c:/path2/path2'), - new InterpreterQuickPickItem('4', 'c:/path4/path4') - ]; - - assert.equal(actual.length, expected.length, 'Suggestion lengths are different.'); - for (let i = 0; i < expected.length; i += 1) { - assert.equal(actual[i].label, expected[i].label, - `Suggestion label is different at ${i}: exected '${expected[i].label}', found '${actual[i].label}'.`); - assert.equal(actual[i].path, expected[i].path, - `Suggestion path is different at ${i}: exected '${expected[i].path}', found '${actual[i].path}'.`); - } - }); - }); - - test('Update Global settings when there are no workspaces', async () => { - const selector = new TestInterpreterSelector(interpreterService.object, workspace.object, - appShell.object, documentManager.object, new PathUtils(false), - comparer.object, pythonPathUpdater.object, shebangProvider.object, - configurationService.object, commandManager.object); - pythonSettings.setup(p => p.pythonPath).returns(() => 'python'); - const selectedItem: IInterpreterQuickPickItem = { - description: '', detail: '', label: '', - path: 'This is the selected Python path' - }; - - workspace.setup(w => w.workspaceFolders).returns(() => undefined); - - selector.getSuggestions = () => Promise.resolve([]); - appShell.setup(s => s.showQuickPick<IInterpreterQuickPickItem>(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(selectedItem)) - .verifiable(TypeMoq.Times.once()); - pythonPathUpdater.setup(p => p.updatePythonPath(TypeMoq.It.isValue(selectedItem.path), - TypeMoq.It.isValue(ConfigurationTarget.Global), - TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(undefined))) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - - await selector.setInterpreter(); - - appShell.verifyAll(); - workspace.verifyAll(); - pythonPathUpdater.verifyAll(); - }); - test('Update workspace folder settings when there is one workspace folder', async () => { - const selector = new TestInterpreterSelector(interpreterService.object, workspace.object, - appShell.object, documentManager.object, new PathUtils(false), - comparer.object, pythonPathUpdater.object, shebangProvider.object, - configurationService.object, commandManager.object); - pythonSettings.setup(p => p.pythonPath).returns(() => 'python'); - const selectedItem: IInterpreterQuickPickItem = { - description: '', detail: '', label: '', - path: 'This is the selected Python path' - }; - - const folder = { name: 'one', uri: Uri.parse('one'), index: 0 }; - workspace.setup(w => w.workspaceFolders).returns(() => [folder]); - - selector.getSuggestions = () => Promise.resolve([]); - appShell.setup(s => s.showQuickPick<IInterpreterQuickPickItem>(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(selectedItem)) - .verifiable(TypeMoq.Times.once()); - pythonPathUpdater.setup(p => p.updatePythonPath(TypeMoq.It.isValue(selectedItem.path), - TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), - TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(folder.uri))) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - - await selector.setInterpreter(); - - appShell.verifyAll(); - workspace.verifyAll(); - pythonPathUpdater.verifyAll(); - }); - test('Update seleted workspace folder settings when there is more than one workspace folder', async () => { - const selector = new TestInterpreterSelector(interpreterService.object, workspace.object, - appShell.object, documentManager.object, new PathUtils(false), - comparer.object, pythonPathUpdater.object, shebangProvider.object, - configurationService.object, commandManager.object); - pythonSettings.setup(p => p.pythonPath).returns(() => 'python'); - const selectedItem: IInterpreterQuickPickItem = { - description: '', detail: '', label: '', - path: 'This is the selected Python path' - }; - - const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; - const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; - workspace.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); - - selector.getSuggestions = () => Promise.resolve([]); - appShell.setup(s => s.showQuickPick<IInterpreterQuickPickItem>(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(selectedItem)) - .verifiable(TypeMoq.Times.once()); - appShell.setup(s => s.showWorkspaceFolderPick(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(folder2)) - .verifiable(TypeMoq.Times.once()); - pythonPathUpdater.setup(p => p.updatePythonPath(TypeMoq.It.isValue(selectedItem.path), - TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), - TypeMoq.It.isValue('ui'), - TypeMoq.It.isValue(folder2.uri))) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - - await selector.setInterpreter(); - - appShell.verifyAll(); - workspace.verifyAll(); - pythonPathUpdater.verifyAll(); - }); - test('Do not update anything when user does not select a workspace folder and there is more than one workspace folder', async () => { - const selector = new TestInterpreterSelector(interpreterService.object, workspace.object, - appShell.object, documentManager.object, new PathUtils(false), - comparer.object, pythonPathUpdater.object, shebangProvider.object, - configurationService.object, commandManager.object); - - const selectedItem: IInterpreterQuickPickItem = { - description: '', detail: '', label: '', - path: 'This is the selected Python path' - }; - - const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; - const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; - workspace.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); - - selector.getSuggestions = () => Promise.resolve([]); - appShell.setup(s => s.showQuickPick<IInterpreterQuickPickItem>(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(selectedItem)) - .verifiable(TypeMoq.Times.never()); - appShell.setup(s => s.showWorkspaceFolderPick(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - pythonPathUpdater.setup(p => p.updatePythonPath(TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny())) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.never()); - - await selector.setInterpreter(); - - appShell.verifyAll(); - workspace.verifyAll(); - pythonPathUpdater.verifyAll(); - }); -}); 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 new file mode 100644 index 000000000000..a32c794b7dc7 --- /dev/null +++ b/src/test/configuration/interpreterSelector/commands/resetInterpreter.unit.test.ts @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// 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 { 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'; + +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>(); + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + + resetInterpreterCommand = new ResetInterpreterCommand( + pythonPathUpdater.object, + commandManager.object, + appShell.object, + workspace.object, + new PathUtils(false), + configurationService.object, + ); + }); + teardown(() => { + sinon.restore(); + }); + + suite('Test method resetInterpreter()', async () => { + test('Update Global settings when there are no workspaces', async () => { + workspace.setup((w) => w.workspaceFolders).returns(() => undefined); + + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(undefined), + TypeMoq.It.isValue(ConfigurationTarget.Global), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(undefined), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await resetInterpreterCommand.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update workspace folder settings when there is one workspace folder and no workspace file', async () => { + const folder = { name: 'one', uri: Uri.parse('one'), index: 0 }; + workspace.setup((w) => w.workspaceFolders).returns(() => [folder]); + workspace.setup((w) => w.workspaceFile).returns(() => undefined); + + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(undefined), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder.uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await resetInterpreterCommand.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update selected workspace folder settings when there is more than one workspace folder', async () => { + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); + const expectedItems = [ + { label: 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: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri, + detail: 'pythonPath', + }), + ) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(undefined), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder2.uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await resetInterpreterCommand.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update entire workspace settings when there is more than one workspace folder and `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, + 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: Interpreters.clearAtWorkspace, + uri: folder1.uri, + }), + ) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(undefined), + TypeMoq.It.isValue(ConfigurationTarget.Workspace), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder1.uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await resetInterpreterCommand.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('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, + 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(undefined)) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup((p) => + p.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await resetInterpreterCommand.resetInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + }); +}); diff --git a/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts new file mode 100644 index 000000000000..7837245ec9d2 --- /dev/null +++ b/src/test/configuration/interpreterSelector/commands/setInterpreter.unit.test.ts @@ -0,0 +1,1558 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { + ConfigurationTarget, + 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 { Common, InterpreterQuickPickList, Interpreters } from '../../../../client/common/utils/localize'; +import { + IMultiStepInput, + IMultiStepInputFactory, + InputStep, + IQuickPickParameters, +} from '../../../../client/common/utils/multiStepInput'; +import { + EnvGroups, + InterpreterStateArgs, + QuickPickType, + SetInterpreterCommand, +} from '../../../../client/interpreter/configuration/interpreterSelector/commands/setInterpreter'; +import { + IInterpreterQuickPickItem, + IInterpreterSelector, + 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 }; + +suite('Set Interpreter Command', () => { + let workspace: TypeMoq.IMock<IWorkspaceService>; + let interpreterSelector: TypeMoq.IMock<IInterpreterSelector>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let commandManager: TypeMoq.IMock<ICommandManager>; + let pythonPathUpdater: TypeMoq.IMock<IPythonPathUpdaterServiceManager>; + let configurationService: TypeMoq.IMock<IConfigurationService>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let platformService: TypeMoq.IMock<IPlatformService>; + let multiStepInputFactory: TypeMoq.IMock<IMultiStepInputFactory>; + 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>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + pythonPathUpdater = TypeMoq.Mock.ofType<IPythonPathUpdaterServiceManager>(); + configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); + 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( + appShell.object, + new PathUtils(false), + pythonPathUpdater.object, + configurationService.object, + commandManager.object, + multiStepInputFactory.object, + platformService.object, + interpreterSelector.object, + workspace.object, + instance(interpreterService), + ); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('Test method _pickInterpreter()', async () => { + let _enterOrBrowseInterpreterPath: sinon.SinonStub; + let sendTelemetryStub: sinon.SinonStub; + let telemetryEvent: TelemetryEventType | undefined; + + const interpreterPath = 'path/to/interpreter'; + const item: IInterpreterQuickPickItem = { + description: interpreterPath, + detail: '', + 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: `${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.resolves(); + sendTelemetryStub = sinon + .stub(Telemetry, 'sendTelemetryEvent') + .callsFake((eventName: EventName, _, properties: unknown) => { + telemetryEvent = { + eventName, + properties, + }; + }); + interpreterSelector + .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), + pythonPathUpdater.object, + configurationService.object, + commandManager.object, + multiStepInputFactory.object, + platformService.object, + interpreterSelector.object, + workspace.object, + instance(interpreterService), + ); + }); + teardown(() => { + telemetryEvent = undefined; + sinon.restore(); + Telemetry._resetSharedProperties(); + }); + + test('Existing state path must be removed before displaying picker', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined as unknown)); + + await setInterpreterCommand._pickInterpreter(multiStepInput.object, state); + + expect(state.path).to.equal(undefined, ''); + }); + + test('Picker should be displayed with expected items', async () => { + const state: InterpreterStateArgs = { path: 'some path', workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + const 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, + 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('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); + + commandManager.verifyAll(); + }); + + 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())) + .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); + + commandManager.verifyAll(); + }); + + 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())) + .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); + + 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, + }), + ); + }); + }); + + suite('Test method _enterOrBrowseInterpreterPath()', async () => { + const items: QuickPickItem[] = [ + { + label: InterpreterQuickPickList.browsePath.label, + detail: InterpreterQuickPickList.browsePath.detail, + }, + ]; + const expectedParameters = { + placeholder: InterpreterQuickPickList.enterPath.placeholder, + items, + acceptFilterBoxTextAsSelection: true, + }; + 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)) + .returns(() => Promise.resolve((undefined as unknown) as QuickPickItem)) + .verifiable(TypeMoq.Times.once()); + + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); + + multiStepInput.verifyAll(); + }); + + test('If user enters path to interpreter in the filter box, get path and update state', async () => { + const state: InterpreterStateArgs = { path: undefined, workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput + .setup((i) => i.showQuickPick(TypeMoq.It.isAny())) + .returns(() => Promise.resolve('enteredPath')); + + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); + + expect(state.path).to.equal('enteredPath', ''); + }); + + test('If `Browse...` is selected, open the file browser to get path and update state', async () => { + const state: InterpreterStateArgs = { path: undefined, workspace: undefined }; + const expectedPathUri = Uri.parse('browsed path'); + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); + appShell + .setup((a) => a.showOpenDialog(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([expectedPathUri])); + + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state); + + expect(state.path).to.equal(expectedPathUri.fsPath, ''); + }); + + test('If `Browse...` option is selected on Windows, file browser is opened using expected parameters', async () => { + const state: InterpreterStateArgs = { path: undefined, workspace: undefined }; + const filtersKey = 'Executables'; + const filtersObject: { [name: string]: string[] } = {}; + filtersObject[filtersKey] = ['exe']; + const expectedParams = { + filters: filtersObject, + openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, + canSelectMany: false, + title: InterpreterQuickPickList.browsePath.title, + defaultUri: undefined, + }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + multiStepInput.setup((i) => i.showQuickPick(TypeMoq.It.isAny())).returns(() => Promise.resolve(items[0])); + appShell + .setup((a) => a.showOpenDialog(expectedParams as OpenDialogOptions)) + .verifiable(TypeMoq.Times.once()); + platformService.setup((p) => p.isWindows).returns(() => true); + + await setInterpreterCommand._enterOrBrowseInterpreterPath(multiStepInput.object, state).ignoreErrors(); + + appShell.verifyAll(); + }); + + test('If `Browse...` option is selected on non-Windows, file browser is opened using expected parameters', async () => { + const state: InterpreterStateArgs = { path: undefined, workspace: undefined }; + const multiStepInput = TypeMoq.Mock.ofType<IMultiStepInput<InterpreterStateArgs>>(); + const expectedParams = { + filters: undefined, + openLabel: InterpreterQuickPickList.browsePath.openButtonLabel, + canSelectMany: false, + title: InterpreterQuickPickList.browsePath.title, + defaultUri: undefined, + }; + 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(); + }); + + 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 }); + }); + } + }); + }); + + suite('Test method setInterpreter()', async () => { + test('Update Global settings when there are no workspaces', async () => { + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); + const selectedItem: IInterpreterQuickPickItem = { + description: '', + detail: '', + label: '', + path: 'This is the selected Python path', + + interpreter: {} as PythonEnvironment, + }; + + workspace.setup((w) => w.workspaceFolders).returns(() => undefined); + + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => []); + const multiStepInput = { + run: (_: unknown, state: InterpreterStateArgs) => { + state.path = selectedItem.path; + return Promise.resolve(); + }, + }; + 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), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await setInterpreterCommand.setInterpreter(); + + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update workspace folder settings when there is one workspace folder and no workspace file', async () => { + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); + workspace.setup((w) => w.workspaceFile).returns(() => undefined); + const selectedItem: IInterpreterQuickPickItem = { + description: '', + detail: '', + label: '', + path: 'This is the selected Python path', + + 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(() => []); + + const multiStepInput = { + run: (_: unknown, state: InterpreterStateArgs) => { + state.path = selectedItem.path; + return Promise.resolve(); + }, + }; + multiStepInputFactory.setup((f) => f.create()).returns(() => multiStepInput as IMultiStepInput<unknown>); + + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(selectedItem.path), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder.uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await setInterpreterCommand.setInterpreter(); + + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update selected workspace folder settings when there is more than one workspace folder', async () => { + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); + const selectedItem: IInterpreterQuickPickItem = { + description: '', + detail: '', + label: '', + path: 'This is the selected Python path', + + interpreter: {} as PythonEnvironment, + }; + + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); + const expectedItems = [ + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri, + detail: 'python', + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri, + detail: 'python', + }, + { + label: Interpreters.entireWorkspace, + uri: folder1.uri, + }, + ]; + + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => []); + + const multiStepInput = { + run: (_: unknown, state: InterpreterStateArgs) => { + state.path = selectedItem.path; + return Promise.resolve(); + }, + }; + 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, + detail: 'python', + }), + ) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(selectedItem.path), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder2.uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await setInterpreterCommand.setInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Update entire workspace settings when there is more than one workspace folder and `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', + + interpreter: {} as PythonEnvironment, + }; + + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); + const expectedItems = [ + { + label: 'one', + description: path.dirname(folder1.uri.fsPath), + uri: folder1.uri, + detail: 'python', + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri, + detail: 'python', + }, + { + label: Interpreters.entireWorkspace, + uri: folder1.uri, + }, + ]; + + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => [selectedItem]); + const multiStepInput = { + run: (_: unknown, state: InterpreterStateArgs) => { + state.path = selectedItem.path; + return Promise.resolve(); + }, + }; + 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, + }), + ) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(selectedItem.path), + TypeMoq.It.isValue(ConfigurationTarget.Workspace), + TypeMoq.It.isValue('ui'), + TypeMoq.It.isValue(folder1.uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + await setInterpreterCommand.setInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + }); + test('Do not update anything when user does not select a workspace folder and there is more than one workspace folder', async () => { + pythonSettings.setup((p) => p.pythonPath).returns(() => 'python'); + workspace.setup((w) => w.workspaceFolders).returns(() => [folder1, folder2]); + + 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, + detail: 'python', + }, + { + label: 'two', + description: path.dirname(folder2.uri.fsPath), + uri: folder2.uri, + detail: 'python', + }, + { + label: Interpreters.entireWorkspace, + uri: folder1.uri, + }, + ]; + + appShell + .setup((s) => s.showQuickPick(TypeMoq.It.isValue(expectedItems), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + pythonPathUpdater + .setup((p) => + p.updatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + await setInterpreterCommand.setInterpreter(); + + appShell.verifyAll(); + workspace.verifyAll(); + pythonPathUpdater.verifyAll(); + multiStepInputFactory.verifyAll(); + }); + test('Make sure multiStepInput.run is called with the correct arguments', async () => { + const pickInterpreter = sinon.stub(SetInterpreterCommand.prototype, '_pickInterpreter'); + setInterpreterCommand = new SetInterpreterCommand( + appShell.object, + new PathUtils(false), + pythonPathUpdater.object, + configurationService.object, + commandManager.object, + multiStepInputFactory.object, + platformService.object, + interpreterSelector.object, + workspace.object, + instance(interpreterService), + ); + 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', + + interpreter: {} as PythonEnvironment, + }; + + workspace.setup((w) => w.workspaceFolders).returns(() => undefined); + + interpreterSelector.setup((i) => i.getSuggestions(TypeMoq.It.isAny())).returns(() => []); + const multiStepInput = { + run: (inputStepArg: InputStepType, state: InterpreterStateArgs) => { + inputStep = inputStepArg; + state.path = selectedItem.path; + return Promise.resolve(); + }, + }; + 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), + ), + ) + .returns(() => Promise.resolve()); + + await setInterpreterCommand.setInterpreter(); + + expect(inputStep).to.not.equal(undefined, ''); + + assert.ok(pickInterpreter.notCalled); + await inputStep(); + assert.ok(pickInterpreter.calledOnce); + }); + }); +}); diff --git a/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts b/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts new file mode 100644 index 000000000000..2ec20be66990 --- /dev/null +++ b/src/test/configuration/interpreterSelector/interpreterSelector.unit.test.ts @@ -0,0 +1,212 @@ +// 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 { PathUtils } from '../../../client/common/platform/pathUtils'; +import { IFileSystem } from '../../../client/common/platform/types'; +import { Architecture } from '../../../client/common/utils/platform'; +import { EnvironmentTypeComparer } from '../../../client/interpreter/configuration/environmentTypeComparer'; +import { InterpreterSelector } from '../../../client/interpreter/configuration/interpreterSelector/interpreterSelector'; +import { IInterpreterComparer, IInterpreterQuickPickItem } from '../../../client/interpreter/configuration/types'; +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: PythonEnvironment = { + architecture: Architecture.Unknown, + companyDisplayName: '', + displayName: '', + envName: '', + path: '', + envType: EnvironmentType.Unknown, + version: new SemVer('1.0.0-alpha'), + sysPrefix: '', + sysVersion: '', +}; + +class InterpreterQuickPickItem implements IInterpreterQuickPickItem { + public path: string; + + public label: string; + + public description!: string; + + public detail?: string; + + public interpreter = ({} as unknown) as PythonEnvironment; + + constructor(l: string, p: string, d?: string) { + this.path = p; + this.label = l; + this.description = d ?? p; + } +} + +suite('Interpreters - selector', () => { + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let fileSystem: TypeMoq.IMock<IFileSystem>; + let newComparer: TypeMoq.IMock<IInterpreterComparer>; + class TestInterpreterSelector extends InterpreterSelector { + public suggestionToQuickPickItem(suggestion: PythonEnvironment, workspaceUri?: Uri): IInterpreterQuickPickItem { + return super.suggestionToQuickPickItem(suggestion, workspaceUri); + } + } + + let selector: TestInterpreterSelector; + + setup(() => { + 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); + + 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, + newComparer.object, + new PathUtils(isWindows), + ); + + const initial: PythonEnvironment[] = [ + { displayName: '1', path: 'c:/path1/path1', envType: EnvironmentType.Unknown }, + { displayName: '2', path: 'c:/path1/path1', envType: EnvironmentType.Unknown }, + { displayName: '2', path: 'c:/path2/path2', envType: EnvironmentType.Unknown }, + { displayName: '2 (virtualenv)', path: 'c:/path2/path2', envType: EnvironmentType.VirtualEnv }, + { displayName: '3', path: 'c:/path2/path2', envType: EnvironmentType.Unknown }, + { displayName: '4', path: 'c:/path4/path4', envType: EnvironmentType.Conda }, + { + displayName: '5', + path: 'c:/path5/path', + envPath: 'c:/path5/path/to/env', + envType: EnvironmentType.Conda, + }, + ].map((item) => ({ ...info, ...item })); + interpreterService + .setup((x) => x.getAllInterpreters(TypeMoq.It.isAny())) + .returns(() => new Promise((resolve) => resolve(initial))); + + const actual = await selector.getAllSuggestions(undefined); + + const expected: InterpreterQuickPickItem[] = [ + new InterpreterQuickPickItem('1', 'c:/path1/path1'), + new InterpreterQuickPickItem('2', 'c:/path1/path1'), + new InterpreterQuickPickItem('2', 'c:/path2/path2'), + new InterpreterQuickPickItem('2 (virtualenv)', 'c:/path2/path2'), + new InterpreterQuickPickItem('3', 'c:/path2/path2'), + new InterpreterQuickPickItem('4', 'c:/path4/path4'), + new InterpreterQuickPickItem('5', 'c:/path5/path/to/env', 'c:/path5/path/to/env'), + ]; + + assert.strictEqual(actual.length, expected.length, 'Suggestion lengths are different.'); + for (let i = 0; i < expected.length; i += 1) { + assert.strictEqual( + actual[i].label, + expected[i].label, + `Suggestion label is different at ${i}: expected '${expected[i].label}', found '${actual[i].label}'.`, + ); + assert.strictEqual( + actual[i].path, + expected[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('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((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 9f16af205a32..1f2d7b4909cf 100644 --- a/src/test/constants.ts +++ b/src/test/constants.ts @@ -1,29 +1,41 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as path from 'path'; -import { IS_CI_SERVER, IS_CI_SERVER_TEST_DEBUGGER } from './ciConstants'; - -export const TEST_TIMEOUT = 25000; -export const IS_SMOKE_TEST = process.env.VSC_PYTHON_SMOKE_TEST === '1'; -export const IS_PERF_TEST = process.env.VSC_PYTHON_PERF_TEST === '1'; -export const IS_MULTI_ROOT_TEST = isMultitrootTest(); - -// If running on CI server, then run debugger tests ONLY if the corresponding flag is enabled. -export const TEST_DEBUGGER = IS_CI_SERVER ? IS_CI_SERVER_TEST_DEBUGGER : true; - -function isMultitrootTest() { - // No need to run smoke nor perf tests in a multi-root environment. - if (IS_SMOKE_TEST || IS_PERF_TEST) { - return false; - } - // tslint:disable-next-line:no-require-imports - const vscode = require('vscode'); - const workspace = vscode.workspace; - return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 1; -} - -export const EXTENSION_ROOT_DIR_FOR_TESTS = path.join(__dirname, '..', '..'); -export const PVSC_EXTENSION_ID_FOR_TESTS = 'ms-python.python'; - -export const SMOKE_TEST_EXTENSIONS_DIR = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'tmp', 'ext', 'smokeTestExtensionsFolder'); +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { IS_CI_SERVER, IS_CI_SERVER_TEST_DEBUGGER } from './ciConstants'; + +// Activating extension for Multiroot and Debugger CI tests for Windows takes just over 2 minutes sometimes, so 3 minutes seems like a safe margin +export const MAX_EXTENSION_ACTIVATION_TIME = 180_000; +export const TEST_TIMEOUT = 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'; +export const IS_MULTI_ROOT_TEST = isMultitrootTest(); + +// If running on CI server, then run debugger tests ONLY if the corresponding flag is enabled. +export const TEST_DEBUGGER = IS_CI_SERVER ? IS_CI_SERVER_TEST_DEBUGGER : true; + +function isMultitrootTest() { + // No need to run smoke nor perf tests in a multi-root environment. + if (IS_SMOKE_TEST || IS_PERF_TEST) { + return false; + } + try { + const vscode = require('vscode'); + const workspace = vscode.workspace; + return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 1; + } catch { + // being accessed, when VS Code hasn't been launched. + return false; + } +} + +export const EXTENSION_ROOT_DIR_FOR_TESTS = path.join(__dirname, '..', '..'); +export const PVSC_EXTENSION_ID_FOR_TESTS = 'ms-python.python'; + +export const SMOKE_TEST_EXTENSIONS_DIR = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'tmp', + 'ext', + 'smokeTestExtensionsFolder', +); diff --git a/src/test/core.ts b/src/test/core.ts index da151a8d089c..3308eecdb21d 100644 --- a/src/test/core.ts +++ b/src/test/core.ts @@ -6,10 +6,9 @@ // File without any dependencies on VS Code. export async function sleep(milliseconds: number) { - return new Promise<void>(resolve => setTimeout(resolve, milliseconds)); + return new Promise<void>((resolve) => setTimeout(resolve, milliseconds)); } -// tslint:disable-next-line:no-empty -export function noop() { } +export function noop() {} export const isWindows = /^win/.test(process.platform); 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/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts deleted file mode 100644 index 334706a0b905..000000000000 --- a/src/test/datascience/dataScienceIocContainer.ts +++ /dev/null @@ -1,380 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -//tslint:disable:trailing-comma -import * as child_process from 'child_process'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { Disposable, Event, EventEmitter, FileSystemWatcher, Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; - -import { - IApplicationShell, - ICommandManager, - IDocumentManager, - IWorkspaceService, -} from '../../client/common/application/types'; -import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; -import { PythonSettings } from '../../client/common/configSettings'; -import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import { PersistentStateFactory } from '../../client/common/persistentState'; -import { IS_WINDOWS } from '../../client/common/platform/constants'; -import { PathUtils } from '../../client/common/platform/pathUtils'; -import { RegistryImplementation } from '../../client/common/platform/registry'; -import { IPlatformService, IRegistry } from '../../client/common/platform/types'; -import { CurrentProcess } from '../../client/common/process/currentProcess'; -import { BufferDecoder } from '../../client/common/process/decoder'; -import { ProcessServiceFactory } from '../../client/common/process/processFactory'; -import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; -import { IBufferDecoder, IProcessServiceFactory, IPythonExecutionFactory } from '../../client/common/process/types'; -import { Bash } from '../../client/common/terminal/environmentActivationProviders/bash'; -import { CommandPromptAndPowerShell } from '../../client/common/terminal/environmentActivationProviders/commandPrompt'; -import { - PyEnvActivationCommandProvider, -} from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; -import { ITerminalActivationCommandProvider } from '../../client/common/terminal/types'; -import { - IAsyncDisposableRegistry, - IConfigurationService, - ICurrentProcess, - ILogger, - IPathUtils, - IPersistentStateFactory, - IsWindows -} from '../../client/common/types'; -import { noop } from '../../client/common/utils/misc'; -import { EnvironmentVariablesService } from '../../client/common/variables/environment'; -import { SystemVariables } from '../../client/common/variables/systemVariables'; -import { IEnvironmentVariablesProvider, IEnvironmentVariablesService } from '../../client/common/variables/types'; -import { CodeCssGenerator } from '../../client/datascience/codeCssGenerator'; -import { History } from '../../client/datascience/history'; -import { HistoryProvider } from '../../client/datascience/historyProvider'; -import { JupyterExecution } from '../../client/datascience/jupyter/jupyterExecution'; -import { JupyterExporter } from '../../client/datascience/jupyter/jupyterExporter'; -import { JupyterImporter } from '../../client/datascience/jupyter/jupyterImporter'; -import { JupyterServer } from '../../client/datascience/jupyter/jupyterServer'; -import { JupyterSessionManager } from '../../client/datascience/jupyter/jupyterSessionManager'; -import { StatusProvider } from '../../client/datascience/statusProvider'; -import { - ICodeCssGenerator, - IHistory, - IHistoryProvider, - IJupyterExecution, - IJupyterSessionManager, - INotebookExporter, - INotebookImporter, - INotebookServer, - IStatusProvider, -} from '../../client/datascience/types'; -import { InterpreterComparer } from '../../client/interpreter/configuration/interpreterComparer'; -import { PythonPathUpdaterService } from '../../client/interpreter/configuration/pythonPathUpdaterService'; -import { PythonPathUpdaterServiceFactory } from '../../client/interpreter/configuration/pythonPathUpdaterServiceFactory'; -import { - IInterpreterComparer, - IPythonPathUpdaterServiceFactory, - IPythonPathUpdaterServiceManager, -} from '../../client/interpreter/configuration/types'; -import { - CONDA_ENV_FILE_SERVICE, - CONDA_ENV_SERVICE, - CURRENT_PATH_SERVICE, - GLOBAL_VIRTUAL_ENV_SERVICE, - ICondaService, - IInterpreterDisplay, - IInterpreterHelper, - IInterpreterLocatorHelper, - IInterpreterLocatorService, - IInterpreterService, - IInterpreterVersionService, - IInterpreterWatcher, - IInterpreterWatcherBuilder, - IKnownSearchPathsForInterpreters, - INTERPRETER_LOCATOR_SERVICE, - IPipEnvService, - IVirtualEnvironmentsSearchPathProvider, - KNOWN_PATH_SERVICE, - PIPENV_SERVICE, - WINDOWS_REGISTRY_SERVICE, - WORKSPACE_VIRTUAL_ENV_SERVICE, -} from '../../client/interpreter/contracts'; -import { InterpreterHelper } from '../../client/interpreter/helpers'; -import { InterpreterService } from '../../client/interpreter/interpreterService'; -import { InterpreterVersionService } from '../../client/interpreter/interpreterVersion'; -import { PythonInterpreterLocatorService } from '../../client/interpreter/locators'; -import { InterpreterLocatorHelper } from '../../client/interpreter/locators/helpers'; -import { CondaEnvFileService } from '../../client/interpreter/locators/services/condaEnvFileService'; -import { CondaEnvService } from '../../client/interpreter/locators/services/condaEnvService'; -import { - CurrentPathService, - PythonInPathCommandProvider, -} from '../../client/interpreter/locators/services/currentPathService'; -import { - GlobalVirtualEnvironmentsSearchPathProvider, - GlobalVirtualEnvService, -} from '../../client/interpreter/locators/services/globalVirtualEnvService'; -import { InterpreterWatcherBuilder } from '../../client/interpreter/locators/services/interpreterWatcherBuilder'; -import { - KnownPathsService, - KnownSearchPathsForInterpreters, -} from '../../client/interpreter/locators/services/KnownPathsService'; -import { PipEnvService } from '../../client/interpreter/locators/services/pipEnvService'; -import { WindowsRegistryService } from '../../client/interpreter/locators/services/windowsRegistryService'; -import { - WorkspaceVirtualEnvironmentsSearchPathProvider, - WorkspaceVirtualEnvService, -} from '../../client/interpreter/locators/services/workspaceVirtualEnvService'; -import { - WorkspaceVirtualEnvWatcherService, -} from '../../client/interpreter/locators/services/workspaceVirtualEnvWatcherService'; -import { IPythonInPathCommandProvider } from '../../client/interpreter/locators/types'; -import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; -import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; -import { UnitTestIocContainer } from '../unittests/serviceRegistry'; -import { MockCommandManager } from './mockCommandManager'; -import { MockJupyterManager } from './mockJupyterManager'; - -export class DataScienceIocContainer extends UnitTestIocContainer { - - private pythonSettings: PythonSettings = new PythonSettings(undefined, new MockAutoSelectionService()); - private commandManager: MockCommandManager = new MockCommandManager(); - private setContexts: { [name: string]: boolean } = {}; - private contextSetEvent: EventEmitter<{ name: string; value: boolean }> = new EventEmitter<{ name: string; value: boolean }>(); - private jupyterMock: MockJupyterManager | undefined; - private shouldMockJupyter: boolean; - private asyncRegistry: AsyncDisposableRegistry; - - constructor() { - super(); - const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; - this.shouldMockJupyter = !isRollingBuild; - this.asyncRegistry = new AsyncDisposableRegistry(); - } - - public get onContextSet(): Event<{ name: string; value: boolean }> { - return this.contextSetEvent.event; - } - - public async dispose(): Promise<void> { - await this.asyncRegistry.dispose(); - await super.dispose(); - } - - //tslint:disable:max-func-body-length - public registerDataScienceTypes() { - this.registerFileSystemTypes(); - this.serviceManager.addSingleton<IJupyterExecution>(IJupyterExecution, JupyterExecution); - this.serviceManager.addSingleton<IHistoryProvider>(IHistoryProvider, HistoryProvider); - this.serviceManager.add<IHistory>(IHistory, History); - this.serviceManager.add<INotebookImporter>(INotebookImporter, JupyterImporter); - this.serviceManager.add<INotebookExporter>(INotebookExporter, JupyterExporter); - this.serviceManager.add<INotebookServer>(INotebookServer, JupyterServer); - this.serviceManager.addSingleton<ICodeCssGenerator>(ICodeCssGenerator, CodeCssGenerator); - this.serviceManager.addSingleton<IStatusProvider>(IStatusProvider, StatusProvider); - this.serviceManager.add<IKnownSearchPathsForInterpreters>(IKnownSearchPathsForInterpreters, KnownSearchPathsForInterpreters); - this.serviceManager.addSingletonInstance<IAsyncDisposableRegistry>(IAsyncDisposableRegistry, this.asyncRegistry); - this.serviceManager.addSingleton<IPythonInPathCommandProvider>(IPythonInPathCommandProvider, PythonInPathCommandProvider); - - // 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); - - // Also setup a mock execution service and interpreter service - const logger = TypeMoq.Mock.ofType<ILogger>(); - const condaService = TypeMoq.Mock.ofType<ICondaService>(); - const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - const documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - const configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); - const interpreterDisplay = TypeMoq.Mock.ofType<IInterpreterDisplay>(); - - // Setup default settings - this.pythonSettings.datascience = { - allowImportFromNotebook: true, - jupyterLaunchTimeout: 20000, - enabled: true, - jupyterServerURI: 'local', - notebookFileRoot: 'WORKSPACE', - changeDirOnImportExport: true, - useDefaultConfigForJupyter: true, - jupyterInterruptTimeout: 10000, - searchForJupyter: true, - }; - - const workspaceConfig: TypeMoq.IMock<WorkspaceConfiguration> = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - workspaceConfig.setup(ws => ws.has(TypeMoq.It.isAnyString())) - .returns(() => false); - workspaceConfig.setup(ws => ws.get(TypeMoq.It.isAnyString())) - .returns(() => undefined); - workspaceConfig.setup(ws => ws.get(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) - .returns((s, d) => d); - - configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => this.pythonSettings); - 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); - interpreterDisplay.setup(i => i.refresh(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - - 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(); - } - } - workspaceService.setup(w => w.createFileSystemWatcher(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { - return new MockFileSystemWatcher(); - }); - workspaceService - .setup(w => w.hasWorkspaceFolders) - .returns(() => true); - const testWorkspaceFolder = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); - const workspaceFolder = this.createMoqWorkspaceFolder(testWorkspaceFolder); - workspaceService - .setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder]); - workspaceService.setup(w => w.rootPath).returns(() => '~'); - - const systemVariables: SystemVariables = new SystemVariables(undefined); - const env = { ...systemVariables }; - - // Look on the path for python - const pythonPath = this.findPythonPath(); - - this.pythonSettings.pythonPath = pythonPath; - const folders = ['Envs', '.virtualenvs']; - this.pythonSettings.venvFolders = folders; - this.pythonSettings.venvPath = path.join('~', 'foo'); - - condaService.setup(c => c.isCondaAvailable()).returns(() => Promise.resolve(false)); - condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isValue(pythonPath))).returns(() => Promise.resolve(false)); - condaService.setup(c => c.condaEnvironmentsFile).returns(() => undefined); - - const envVarsProvider: TypeMoq.IMock<IEnvironmentVariablesProvider> = TypeMoq.Mock.ofType<IEnvironmentVariablesProvider>(); - envVarsProvider.setup(e => e.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve(env)); - this.serviceManager.addSingleton<IVirtualEnvironmentsSearchPathProvider>(IVirtualEnvironmentsSearchPathProvider, GlobalVirtualEnvironmentsSearchPathProvider, 'global'); - this.serviceManager.addSingleton<IVirtualEnvironmentsSearchPathProvider>(IVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvironmentsSearchPathProvider, 'workspace'); - this.serviceManager.addSingleton<IVirtualEnvironmentManager>(IVirtualEnvironmentManager, VirtualEnvironmentManager); - - this.serviceManager.addSingletonInstance<ILogger>(ILogger, logger.object); - this.serviceManager.addSingletonInstance<ICondaService>(ICondaService, condaService.object); - this.serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, appShell.object); - this.serviceManager.addSingletonInstance<IDocumentManager>(IDocumentManager, documentManager.object); - this.serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, workspaceService.object); - this.serviceManager.addSingletonInstance<IConfigurationService>(IConfigurationService, configurationService.object); - this.serviceManager.addSingleton<IBufferDecoder>(IBufferDecoder, BufferDecoder); - this.serviceManager.addSingleton<IEnvironmentVariablesService>(IEnvironmentVariablesService, EnvironmentVariablesService); - this.serviceManager.addSingletonInstance<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider, envVarsProvider.object); - this.serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils); - this.serviceManager.addSingletonInstance<boolean>(IsWindows, IS_WINDOWS); - - this.serviceManager.add<IInterpreterWatcher>(IInterpreterWatcher, WorkspaceVirtualEnvWatcherService, WORKSPACE_VIRTUAL_ENV_SERVICE); - this.serviceManager.addSingleton<IInterpreterWatcherBuilder>(IInterpreterWatcherBuilder, InterpreterWatcherBuilder); - - this.serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, PythonInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); - this.serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, CondaEnvFileService, CONDA_ENV_FILE_SERVICE); - this.serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, CondaEnvService, CONDA_ENV_SERVICE); - this.serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, CurrentPathService, CURRENT_PATH_SERVICE); - this.serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, GlobalVirtualEnvService, GLOBAL_VIRTUAL_ENV_SERVICE); - this.serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, WorkspaceVirtualEnvService, WORKSPACE_VIRTUAL_ENV_SERVICE); - this.serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, PipEnvService, PIPENV_SERVICE); - this.serviceManager.addSingleton<IInterpreterLocatorService>(IPipEnvService, PipEnvService); - this.serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, WindowsRegistryService, WINDOWS_REGISTRY_SERVICE); - - this.serviceManager.addSingleton<IInterpreterLocatorService>(IInterpreterLocatorService, KnownPathsService, KNOWN_PATH_SERVICE); - - this.serviceManager.addSingleton<IInterpreterHelper>(IInterpreterHelper, InterpreterHelper); - this.serviceManager.addSingleton<IInterpreterLocatorHelper>(IInterpreterLocatorHelper, InterpreterLocatorHelper); - this.serviceManager.addSingleton<IInterpreterComparer>(IInterpreterComparer, InterpreterComparer); - this.serviceManager.addSingleton<IInterpreterVersionService>(IInterpreterVersionService, InterpreterVersionService); - this.serviceManager.addSingleton<IPersistentStateFactory>(IPersistentStateFactory, PersistentStateFactory); - this.serviceManager.addSingletonInstance<IInterpreterDisplay>(IInterpreterDisplay, interpreterDisplay.object); - - this.serviceManager.addSingleton<IPythonPathUpdaterServiceFactory>(IPythonPathUpdaterServiceFactory, PythonPathUpdaterServiceFactory); - this.serviceManager.addSingleton<IPythonPathUpdaterServiceManager>(IPythonPathUpdaterServiceManager, PythonPathUpdaterService); - this.serviceManager.addSingleton<IRegistry>(IRegistry, RegistryImplementation); - - const currentProcess = new CurrentProcess(); - this.serviceManager.addSingletonInstance<ICurrentProcess>(ICurrentProcess, currentProcess); - - // Create our jupyter mock if necessary - if (this.shouldMockJupyter) { - this.jupyterMock = new MockJupyterManager(this.serviceManager); - } else { - this.serviceManager.addSingleton<IProcessServiceFactory>(IProcessServiceFactory, ProcessServiceFactory); - this.serviceManager.addSingleton<IPythonExecutionFactory>(IPythonExecutionFactory, PythonExecutionFactory); - this.serviceManager.addSingleton<IInterpreterService>(IInterpreterService, InterpreterService); - this.serviceManager.addSingleton<IJupyterSessionManager>(IJupyterSessionManager, JupyterSessionManager); - } - - if (this.serviceManager.get<IPlatformService>(IPlatformService).isWindows) { - this.serviceManager.addSingleton<IRegistry>(IRegistry, RegistryImplementation); - } - this.serviceManager.addSingleton<ITerminalActivationCommandProvider>( - ITerminalActivationCommandProvider, Bash, 'bashCShellFish'); - this.serviceManager.addSingleton<ITerminalActivationCommandProvider>( - ITerminalActivationCommandProvider, CommandPromptAndPowerShell, 'commandPromptAndPowerShell'); - this.serviceManager.addSingleton<ITerminalActivationCommandProvider>( - ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, 'pyenv'); - - const dummyDisposable = { - dispose: () => { return; } - }; - - appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAnyString())).returns(() => Promise.resolve('')); - appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('')); - appShell.setup(a => a.showSaveDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve(Uri.file(''))); - appShell.setup(a => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); - - // tslint:disable-next-line:no-empty no-console - logger.setup(l => l.logInformation(TypeMoq.It.isAny())).returns((m) => console.log(m)); - - const interpreterManager = this.serviceContainer.get<IInterpreterService>(IInterpreterService); - interpreterManager.initialize(); - } - - public createMoqWorkspaceFolder(folderPath: string) { - const folder = TypeMoq.Mock.ofType<WorkspaceFolder>(); - folder.setup(f => f.uri).returns(() => Uri.file(folderPath)); - return folder.object; - } - - public getContext(name: string): boolean { - if (this.setContexts.hasOwnProperty(name)) { - return this.setContexts[name]; - } - - return false; - } - - public forceSettingsChanged() { - this.pythonSettings.emit('change'); - } - - public get mockJupyter(): MockJupyterManager | undefined { - return this.jupyterMock; - } - - private findPythonPath(): string { - try { - const output = child_process.execFileSync('python', ['-c', 'import sys;print(sys.executable)'], { encoding: 'utf8' }); - return output.replace(/\r?\n/g, ''); - } 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 25e7bf296951..000000000000 --- a/src/test/datascience/datascience.unit.test.ts +++ /dev/null @@ -1,55 +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 { IApplicationShell, ICommandManager, IDocumentManager } from '../../client/common/application/types'; -import { IConfigurationService, IDisposableRegistry, IExtensionContext } from '../../client/common/types'; -import { formatStreamText } from '../../client/datascience/common'; -import { DataScience } from '../../client/datascience/datascience'; -import { IDataScience, IDataScienceCodeLensProvider } from '../../client/datascience/types'; -import { IServiceContainer } from '../../client/ioc/types'; - -suite('Data Science Tests', () => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let shell: TypeMoq.IMock<IApplicationShell>; - let commandManager: TypeMoq.IMock<ICommandManager>; - let disposableRegistry: TypeMoq.IMock<IDisposableRegistry>; - let extensionContext: TypeMoq.IMock<IExtensionContext>; - let codeLensProvider: TypeMoq.IMock<IDataScienceCodeLensProvider>; - let configurationService: TypeMoq.IMock<IConfigurationService>; - let documentManager: TypeMoq.IMock<IDocumentManager>; - let dataScience: IDataScience; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - commandManager = TypeMoq.Mock.ofType<ICommandManager>(); - disposableRegistry = TypeMoq.Mock.ofType<IDisposableRegistry>(); - shell = TypeMoq.Mock.ofType<IApplicationShell>(); - extensionContext = TypeMoq.Mock.ofType<IExtensionContext>(); - codeLensProvider = TypeMoq.Mock.ofType<IDataScienceCodeLensProvider>(); - configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); - documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - dataScience = new DataScience( - serviceContainer.object, - commandManager.object, - disposableRegistry.object, - extensionContext.object, - codeLensProvider.object, - configurationService.object, - documentManager.object, - shell.object); - }); - - 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/datascienceSurveyBanner.unit.test.ts b/src/test/datascience/datascienceSurveyBanner.unit.test.ts deleted file mode 100644 index 78d850ae6aed..000000000000 --- a/src/test/datascience/datascienceSurveyBanner.unit.test.ts +++ /dev/null @@ -1,111 +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 { IApplicationShell } from '../../client/common/application/types'; -import { IBrowserService, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; -import { DataScienceSurveyBanner, DSSurveyStateKeys } from '../../client/datascience/dataScienceSurveyBanner'; - -suite('Data Science 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('Data science banner should be enabled after we hit our command execution count', async () => { - const enabledValue: boolean = true; - const attemptCounter: number = 1000; - const testBanner: DataScienceSurveyBanner = preparePopup(attemptCounter, enabledValue, 0, 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 attemptCounter: number = 0; - const testBanner: DataScienceSurveyBanner = preparePopup(attemptCounter, enabledValue, 0, appShell.object, browser.object, targetUri); - testBanner.showBanner().ignoreErrors(); - }); - test('Do not show data science banner if we have not hit our command 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 attemptCounter: number = 100; - const testBanner: DataScienceSurveyBanner = preparePopup(attemptCounter, enabledValue, 1000, appShell.object, browser.object, targetUri); - testBanner.showBanner().ignoreErrors(); - }); -}); - -function preparePopup( - commandCounter: number, - enabledValue: boolean, - commandThreshold: number, - appShell: IApplicationShell, - browser: IBrowserService, - targetUri: string -): DataScienceSurveyBanner { - const myfactory: typemoq.IMock<IPersistentStateFactory> = typemoq.Mock.ofType<IPersistentStateFactory>(); - const enabledValState: typemoq.IMock<IPersistentState<boolean>> = typemoq.Mock.ofType<IPersistentState<boolean>>(); - const attemptCountState: typemoq.IMock<IPersistentState<number>> = typemoq.Mock.ofType<IPersistentState<number>>(); - 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(); - }); - - attemptCountState.setup(a => a.updateValue(typemoq.It.isAnyNumber())).returns(() => { - commandCounter += 1; - return Promise.resolve(); - }); - - enabledValState.setup(a => a.value).returns(() => enabledValue); - attemptCountState.setup(a => a.value).returns(() => commandCounter); - - 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.ShowAttemptCounter), - typemoq.It.isAnyNumber())).returns(() => { - return attemptCountState.object; - }); - return new DataScienceSurveyBanner(appShell, myfactory.object, browser, commandThreshold, targetUri); -} 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 44dc8d7016fa..000000000000 --- a/src/test/datascience/editor-integration/codelensprovider.unit.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as TypeMoq from 'typemoq'; -import { TextDocument } from 'vscode'; -import { IConfigurationService, IDataScienceSettings, IPythonSettings } from '../../../client/common/types'; -import { DataScienceCodeLensProvider } from '../../../client/datascience/editor-integration/codelensprovider'; -import { ICodeWatcher, IDataScienceCodeLensProvider } from '../../../client/datascience/types'; -import { IServiceContainer } from '../../../client/ioc/types'; - -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>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); - - pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - dataScienceSettings = TypeMoq.Mock.ofType<IDataScienceSettings>(); - 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); - - codeLensProvider = new DataScienceCodeLensProvider(serviceContainer.object, configurationService.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()); - - codeLensProvider.provideCodeLenses(document.object, undefined); - - targetCodeWatcher.verifyAll(); - serviceContainer.verifyAll(); - }); - - test('Initialize Code Lenses same doc called', () => { - // 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.exactly(2)); - targetCodeWatcher.setup(tc => tc.getFileName()).returns(() => 'test.py'); - targetCodeWatcher.setup(tc => tc.getVersion()).returns(() => 1); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICodeWatcher))).returns(() => targetCodeWatcher.object).verifiable(TypeMoq.Times.once()); - - codeLensProvider.provideCodeLenses(document.object, undefined); - codeLensProvider.provideCodeLenses(document.object, undefined); - - // 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.getFileName()).returns(() => '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)); - - codeLensProvider.provideCodeLenses(document.object, undefined); - codeLensProvider.provideCodeLenses(document2.object, undefined); - codeLensProvider.provideCodeLenses(document3.object, undefined); - - // 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 bab7226b9336..000000000000 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ /dev/null @@ -1,316 +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 -// Disable whitespace / multiline as we use that to pass in our fake file strings - -import { expect } from 'chai'; -import * as TypeMoq from 'typemoq'; -import { Range, Selection, TextEditor } from 'vscode'; - -import { IApplicationShell, ICommandManager, IDocumentManager } from '../../../client/common/application/types'; -import { ILogger } from '../../../client/common/types'; -import { Commands } from '../../../client/datascience/constants'; -import { CodeWatcher } from '../../../client/datascience/editor-integration/codewatcher'; -import { IHistory, IHistoryProvider } from '../../../client/datascience/types'; -import { createDocument } from './helpers'; - -suite('DataScience Code Watcher Unit Tests', () => { - let codeWatcher: CodeWatcher; - let commandManager: TypeMoq.IMock<ICommandManager>; - let appShell: TypeMoq.IMock<IApplicationShell>; - let logger: TypeMoq.IMock<ILogger>; - let historyProvider: TypeMoq.IMock<IHistoryProvider>; - let activeHistory: TypeMoq.IMock<IHistory>; - let documentManager: TypeMoq.IMock<IDocumentManager>; - let textEditor: TypeMoq.IMock<TextEditor>; - setup(() => { - commandManager = TypeMoq.Mock.ofType<ICommandManager>(); - appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - logger = TypeMoq.Mock.ofType<ILogger>(); - historyProvider = TypeMoq.Mock.ofType<IHistoryProvider>(); - activeHistory = TypeMoq.Mock.ofType<IHistory>(); - documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - textEditor = TypeMoq.Mock.ofType<TextEditor>(); - - // Setup our active history instance - historyProvider.setup(h => h.getOrCreateActive()).returns(() => activeHistory.object); - - // Setup our active text editor - documentManager.setup(dm => dm.activeTextEditor).returns(() => textEditor.object); - - codeWatcher = new CodeWatcher(appShell.object, logger.object, historyProvider.object, documentManager.object); - }); - - test('Add a file with just a #%% mark to a code watcher', () => { - const fileName = 'test.py'; - const version = 1; - const inputText = `#%%`; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - - codeWatcher.addFile(document.object); - - // Verify meta data - expect(codeWatcher.getFileName()).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(2, 'Incorrect count of code lenses'); - if (codeLenses[0].command) { - expect(codeLenses[0].command.command).to.be.equal(Commands.RunCell, 'Run Cell code lens command incorrect'); - } - expect(codeLenses[0].range).to.be.deep.equal(new Range(0, 0, 0, 3), 'Run Cell code lens range incorrect'); - if (codeLenses[1].command) { - expect(codeLenses[1].command.command).to.be.equal(Commands.RunAllCells, 'Run All Cells code lens command incorrect'); - } - expect(codeLenses[1].range).to.be.deep.equal(new Range(0, 0, 0, 3), 'Run All Cells code lens range incorrect'); - - // Verify function calls - document.verifyAll(); - }); - - test('Add a file without a mark to a code watcher', () => { - const fileName = 'test.py'; - const version = 1; - const inputText = `dummy`; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - - codeWatcher.addFile(document.object); - - // Verify meta data - expect(codeWatcher.getFileName()).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 = 'test.py'; - const version = 1; - const inputText = -`first line -second line - -#%% -third line - -#%% -fourth line`; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - - codeWatcher.addFile(document.object); - - // Verify meta data - expect(codeWatcher.getFileName()).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(4, 'Incorrect count of code lenses'); - if (codeLenses[0].command) { - expect(codeLenses[0].command.command).to.be.equal(Commands.RunCell, 'Run Cell code lens command incorrect'); - } - expect(codeLenses[0].range).to.be.deep.equal(new Range(3, 0, 5, 0), 'Run Cell code lens range incorrect'); - if (codeLenses[1].command) { - expect(codeLenses[1].command.command).to.be.equal(Commands.RunAllCells, 'Run All Cells code lens command incorrect'); - } - expect(codeLenses[1].range).to.be.deep.equal(new Range(3, 0, 5, 0), 'Run All Cells code lens range incorrect'); - if (codeLenses[2].command) { - expect(codeLenses[2].command.command).to.be.equal(Commands.RunCell, 'Run Cell code lens command incorrect'); - } - expect(codeLenses[2].range).to.be.deep.equal(new Range(6, 0, 7, 11), 'Run Cell code lens range incorrect'); - if (codeLenses[3].command) { - expect(codeLenses[3].command.command).to.be.equal(Commands.RunAllCells, 'Run All Cells code lens command incorrect'); - } - expect(codeLenses[3].range).to.be.deep.equal(new Range(6, 0, 7, 11), 'Run All Cells code lens range incorrect'); - - // Verify function calls - document.verifyAll(); - }); - - test('Test the RunCell command', async () => { - const fileName = 'test.py'; - const version = 1; - const inputText = ''; // This test overrides getText, so we don't need to fill this in - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - - // Specify our range and text here - const testRange = new Range(0, 0, 10, 10); - const testString = 'testing'; - document.setup(doc => doc.getText(testRange)).returns(() => testString).verifiable(TypeMoq.Times.once()); - - codeWatcher.addFile(document.object); - - // Set up our expected call to add code - activeHistory.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; - }))).verifiable(TypeMoq.Times.once()); - - // Try our RunCell command - await codeWatcher.runCell(testRange); - - // Verify function calls - activeHistory.verifyAll(); - document.verifyAll(); - }); - - test('Test the RunAllCells command', async () => { - const fileName = 'test.py'; - const version = 1; - const inputText = -`#%% -testing1 -#%% -testing2`; // Command tests override getText, so just need the ranges here - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - - // Specify our range and text here - const testRange1 = new Range(0, 0, 1, 8); - const testString1 = 'testing1'; - document.setup(doc => doc.getText(testRange1)).returns(() => testString1).verifiable(TypeMoq.Times.once()); - const testRange2 = new Range(2, 0, 3, 8); - const testString2 = 'testing2'; - document.setup(doc => doc.getText(testRange2)).returns(() => testString2).verifiable(TypeMoq.Times.once()); - - codeWatcher.addFile(document.object); - - // Set up our expected calls to add code - activeHistory.setup(h => h.addCode(TypeMoq.It.isValue(testString1), - TypeMoq.It.isValue('test.py'), - TypeMoq.It.isValue(0) - )).verifiable(TypeMoq.Times.once()); - - activeHistory.setup(h => h.addCode(TypeMoq.It.isValue(testString2), - TypeMoq.It.isValue('test.py'), - TypeMoq.It.isValue(2) - )).verifiable(TypeMoq.Times.once()); - - // Try our RunCell command - await codeWatcher.runAllCells(); - - // Verify function calls - activeHistory.verifyAll(); - document.verifyAll(); - }); - - test('Test the RunCurrentCell command', async () => { - const fileName = 'test.py'; - const version = 1; - const inputText = -`#%% -testing1 -#%% -testing2`; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - document.setup(d => d.getText(new Range(2, 0, 3, 8))).returns(() => 'testing2').verifiable(TypeMoq.Times.atLeastOnce()); - - codeWatcher.addFile(document.object); - - // Set up our expected calls to add code - activeHistory.setup(h => h.addCode(TypeMoq.It.isValue('testing2'), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(2), - TypeMoq.It.is((ed: TextEditor) => { - return textEditor.object === ed; - }))).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)); - - // Try our RunCell command with the first selection point - await codeWatcher.runCurrentCell(); - - // Verify function calls - activeHistory.verifyAll(); - document.verifyAll(); - }); - - test('Test the RunCurrentCell command outside of a cell', async () => { - const fileName = 'test.py'; - const version = 1; - const inputText = -`testing1 -#%% -testing2`; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - - codeWatcher.addFile(document.object); - - // We don't want to ever call add code here - activeHistory.setup(h => h.addCode(TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny())).verifiable(TypeMoq.Times.never()); - - // For this test we need to set up a document selection point - textEditor.setup(te => te.selection).returns(() => new Selection(0, 0, 0, 0)); - - // Try our RunCell command with the first selection point - await codeWatcher.runCurrentCell(); - - // Verify function calls - activeHistory.verifyAll(); - document.verifyAll(); - }); - - test('Test the RunCellAndAdvance command with next cell', async () => { - const fileName = 'test.py'; - const version = 1; - const inputText = -`#%% -testing1 -#%% -testing2`; // Command tests override getText, so just need the ranges here - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - const testRange = new Range(0, 0, 1, 8); - const testString = 'testing1'; - document.setup(d => d.getText(testRange)).returns(() => testString).verifiable(TypeMoq.Times.atLeastOnce()); - - codeWatcher.addFile(document.object); - - // Set up our expected calls to add code - activeHistory.setup(h => h.addCode(TypeMoq.It.isValue(testString), - TypeMoq.It.isValue('test.py'), - TypeMoq.It.isValue(0), - TypeMoq.It.is((ed: TextEditor) => { - return textEditor.object === ed; - }))).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['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(); - activeHistory.verifyAll(); - document.verifyAll(); - }); -}); diff --git a/src/test/datascience/editor-integration/helpers.ts b/src/test/datascience/editor-integration/helpers.ts deleted file mode 100644 index d36b17992e76..000000000000 --- a/src/test/datascience/editor-integration/helpers.ts +++ /dev/null @@ -1,52 +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 } 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(() => fileName).verifiable(times); - document.setup(d => d.version).returns(() => fileVersion).verifiable(times); - - // Next add the lines in - document.setup(d => d.lineCount).returns(() => inputLines.length).verifiable(times); - - inputLines.forEach((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); - document.setup(d => d.lineAt(TypeMoq.It.isValue(index))).returns(() => textLine.object).verifiable(TypeMoq.Times.atLeastOnce()); - }); - - // Get text is a bit trickier - if (implementGetText) { - document.setup(d => d.getText(TypeMoq.It.isAny())).returns((r: Range) => { - let results = ''; - for (let line = r.start.line; line <= r.end.line; 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'; - } - } - return results; - }); - } - - return document; -} diff --git a/src/test/datascience/execution.unit.test.ts b/src/test/datascience/execution.unit.test.ts deleted file mode 100644 index 90be9b49bc89..000000000000 --- a/src/test/datascience/execution.unit.test.ts +++ /dev/null @@ -1,571 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { JSONObject } from '@phosphor/coreutils/lib/json'; -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 { anyString, anything, instance, match, mock, 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 { ConfigurationChangeEvent, Disposable, EventEmitter } 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 { Logger } from '../../client/common/logger'; -import { FileSystem } from '../../client/common/platform/fileSystem'; -import { IFileSystem, TemporaryFile } from '../../client/common/platform/types'; -import { ProcessServiceFactory } from '../../client/common/process/processFactory'; -import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; -import { - ExecutionResult, - IProcessService, - IPythonExecutionService, - ObservableExecutionResult, - Output -} from '../../client/common/process/types'; -import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, ILogger } from '../../client/common/types'; -import { Architecture } from '../../client/common/utils/platform'; -import { EXTENSION_ROOT_DIR } from '../../client/constants'; -import { JupyterExecution } from '../../client/datascience/jupyter/jupyterExecution'; -import { ICell, IConnection, IJupyterKernelSpec, INotebookServer, InterruptResult } from '../../client/datascience/types'; -import { InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; -import { InterpreterService } from '../../client/interpreter/interpreterService'; -import { CondaService } from '../../client/interpreter/locators/services/condaService'; -import { KnownSearchPathsForInterpreters } from '../../client/interpreter/locators/services/KnownPathsService'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { getOSType, OSType } from '../common'; -import { noop } from '../core'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; -import { MockJupyterManager } from './mockJupyterManager'; - -// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length -class MockJupyterServer implements INotebookServer { - - private conninfo: IConnection | undefined; - private kernelSpec: IJupyterKernelSpec | undefined; - private notebookFile: TemporaryFile | undefined; - public connect(conninfo: IConnection, kernelSpec: IJupyterKernelSpec): Promise<void> { - this.conninfo = conninfo; - this.kernelSpec = kernelSpec; - - // Validate connection info and kernel spec - if (conninfo.baseUrl && kernelSpec.name && /[a-z,A-Z,0-9,-,.,_]+/.test(kernelSpec.name)) { - return Promise.resolve(); - } - return Promise.reject('invalid server startup'); - } - //tslint:disable-next-line:no-any - public onStatusChanged(_listener: (e: boolean) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { - return { dispose: noop }; - } - public getCurrentState(): Promise<ICell[]> { - throw new Error('Method not implemented'); - } - public executeObservable(code: string, f: string, line: number): Observable<ICell[]> { - 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 setInitialDirectory(directory: string): Promise<void> { - throw new Error('Method not implemented'); - } - public getConnectionInfo(): IConnection | undefined { - throw new Error('Method not implemented'); - } - public async shutdown() { - return Promise.resolve(); - } - - public interruptKernel(timeout: number) : Promise<InterruptResult> { - throw new Error('Method not implemented'); - } - - public async dispose() : Promise<void> { - if (this.conninfo) { - this.conninfo.dispose(); // This should kill the process that's running - this.conninfo = undefined; - } - if (this.kernelSpec) { - await this.kernelSpec.dispose(); // This destroy any unwanted kernel specs if necessary - this.kernelSpec = undefined; - } - if (this.notebookFile) { - this.notebookFile.dispose(); // This destroy any unwanted kernel specs if necessary - this.notebookFile = undefined; - } - - } -} - -class DisposableRegistry implements IDisposableRegistry, IAsyncDisposableRegistry { - private disposables: Disposable[] = []; - - public push = (disposable: Disposable): void => { - this.disposables.push(disposable); - } - - public dispose = async () : Promise<void> => { - for (let i = 0; i < this.disposables.length; i += 1) { - const disposable = this.disposables[i]; - if (disposable) { - 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 executionFactory = mock(PythonExecutionFactory); - const condaService = mock(CondaService); - const configService = mock(ConfigurationService); - const processServiceFactory = mock(ProcessServiceFactory); - const knownSearchPaths = mock(KnownSearchPathsForInterpreters); - const logger = mock(Logger); - const fileSystem = mock(FileSystem); - 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; - - 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(() => { - return cleanupDisposables(); - }); - - function cleanupDisposables() : Promise<void> { - return disposableRegistry.dispose(); - } - - 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.Mock.ofType<T>(); - result['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, 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); - } - - 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 setupWorkingPythonService(service: TypeMoq.IMock<IPythonExecutionService>) { - 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)); - } - - function setupMissingKernelPythonService(service: TypeMoq.IMock<IPythonExecutionService>) { - 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)); - } - - 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.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'], () => { - // Return different results after we install our kernel - if (ipykernelInstallCount > 0) { - return Promise.resolve({ stdout: `working ${path.dirname(workingKernelSpec)}\r\n 0e8519db-0895-416c-96df-fa80131ecea0 C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0` }); - } - return Promise.resolve({ stdout: ` 0e8519db-0895-416c-96df-fa80131ecea0 C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0` }); - }); - setupProcessServiceExec(service, workingPython.path, ['-m', 'jupyter', 'kernelspec', 'list'], Promise.resolve({ stdout: `working ${path.dirname(workingKernelSpec)}\r\n 0e8519db-0895-416c-96df-fa80131ecea0 C:\\Users\\rchiodo\\AppData\\Roaming\\jupyter\\kernels\\0e8519db-0895-416c-96df-fa80131ecea0` })); - setupProcessServiceExecWithFunc(service, workingPython.path, ['-m', 'ipykernel', 'install', '--user', '--name', /\w+-\w+-\w+-\w+-\w+/, '--display-name', `'Python Interactive'`], () => { - ipykernelInstallCount += 1; - return Promise.resolve({ stdout: `somename ${path.dirname(workingKernelSpec)}` }); - }); - const getServerInfoPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getServerInfo.py'); - setupProcessServiceExec(service, workingPython.path, [getServerInfoPath], Promise.resolve({ stdout: 'failure to get server infos' })); - setupProcessServiceExecObservable(service, workingPython.path, ['-m', 'jupyter', 'kernelspec', 'list'], [], []); - setupProcessServiceExecObservable(service, workingPython.path, ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/, /.*/], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); - } - - function setupMissingKernelProcessService(service: TypeMoq.IMock<IProcessService>, notebookStdErr?: string[]) { - setupProcessServiceExec(service, missingKernelPython.path, ['-m', 'jupyter', 'kernelspec', 'list'], Promise.resolve({ stdout: `working ${path.dirname(workingKernelSpec)}` })); - const getServerInfoPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getServerInfo.py'); - setupProcessServiceExec(service, missingKernelPython.path, [getServerInfoPath], Promise.resolve({ stdout: 'failure to get server infos' })); - setupProcessServiceExecObservable(service, missingKernelPython.path, ['-m', 'jupyter', 'kernelspec', 'list'], [], []); - setupProcessServiceExecObservable(service, missingKernelPython.path, ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/, /.*/], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); - } - - function setupPathProcessService(jupyterPath: string, service: TypeMoq.IMock<IProcessService>, notebookStdErr?: string[]) { - setupProcessServiceExec(service, jupyterPath, ['kernelspec', 'list'], Promise.resolve({ stdout: `working ${path.dirname(workingKernelSpec)}` })); - setupProcessServiceExecObservable(service, jupyterPath, ['kernelspec', 'list'], [], []); - 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=.*/, /.*/], [], 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[]): JupyterExecution { - // Setup defaults - when(interpreterService.onDidChangeInterpreter).thenReturn(dummyEvent.event); - when(interpreterService.getActiveInterpreter()).thenResolve(activeInterpreter); - when(interpreterService.getInterpreters()).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'); - - // Create our working python and process service. - const workingService = createTypeMoq<IPythonExecutionService>('working'); - setupWorkingPythonService(workingService); - const missingKernelService = createTypeMoq<IPythonExecutionService>('missingKernel'); - setupMissingKernelPythonService(missingKernelService); - 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); - - // Special case, nothing passed in. Match the active - let activeService = workingService.object; - if (activeInterpreter === missingKernelPython) { - activeService = missingKernelService.object; - } else if (activeInterpreter === missingNotebookPython) { - activeService = missingNotebookService.object; - } else if (activeInterpreter === missingNotebookPython2) { - activeService = missingNotebookService2.object; - } - when(executionFactory.create(argThat(o => !o || !o.pythonPath))).thenResolve(activeService); - when(processServiceFactory.create()).thenResolve(processService.object); - - // Service container needs logger, file system, and config service - when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn(instance(configService)); - when(serviceContainer.get<IFileSystem>(IFileSystem)).thenReturn(instance(fileSystem)); - when(serviceContainer.get<ILogger>(ILogger)).thenReturn(instance(logger)); - when(serviceContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspaceService)); - when(configService.getSettings()).thenReturn(pythonSettings); - when(workspaceService.onDidChangeConfiguration).thenReturn(configChangeEvent.event); - - // Setup default settings - pythonSettings.datascience = { - allowImportFromNotebook: true, - jupyterLaunchTimeout: 10, - enabled: true, - jupyterServerURI: 'local', - notebookFileRoot: 'WORKSPACE', - changeDirOnImportExport: true, - useDefaultConfigForJupyter: true, - jupyterInterruptTimeout: 10000, - searchForJupyter: true - }; - - // 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.createTemporaryFile(anything())).thenResolve(tempFile); - when(fileSystem.createDirectory(anything())).thenResolve(); - when(fileSystem.deleteDirectory(anything())).thenResolve(); - - const serviceManager = mock(ServiceManager); - - const mockSessionManager = new MockJupyterManager(instance(serviceManager)); - - return new JupyterExecution( - instance(executionFactory), - instance(condaService), - instance(interpreterService), - instance(processServiceFactory), - instance(knownSearchPaths), - instance(logger), - disposableRegistry, - disposableRegistry, - instance(fileSystem), - mockSessionManager, - instance(workspaceService), - instance(configService), - instance(serviceContainer)); - } - - test('Working notebook and commands found', async () => { - const execution = createExecution(workingPython); - await assert.eventually.equal(execution.isNotebookSupported(), true, 'Notebook not supported'); - await assert.eventually.equal(execution.isImportSupported(), true, 'Import not supported'); - await assert.eventually.equal(execution.isKernelSpecSupported(), true, 'Kernel Spec not supported'); - await assert.eventually.equal(execution.isKernelCreateSupported(), true, 'Kernel Create not supported'); - const usableInterpreter = await execution.getUsableJupyterPython(); - assert.isOk(usableInterpreter, 'Usable intepreter not found'); - await assert.isFulfilled(execution.connectToNotebookServer(undefined, true), 'Should be able to start a server'); - }).timeout(10000); - - test('Failing notebook throws exception', async () => { - const execution = createExecution(missingNotebookPython); - when(interpreterService.getInterpreters()).thenResolve([missingNotebookPython]); - await assert.isRejected(execution.connectToNotebookServer(undefined, true), 'Running cells requires Jupyter notebooks to be installed.'); - }).timeout(10000); - - test('Failing others throws exception', async () => { - const execution = createExecution(missingNotebookPython); - when(interpreterService.getInterpreters()).thenResolve([missingNotebookPython, missingNotebookPython2]); - await assert.isRejected(execution.connectToNotebookServer(undefined, true), 'Running cells requires Jupyter notebooks to be installed.'); - }).timeout(10000); - - test('Slow notebook startups throws exception', async () => { - const execution = createExecution(workingPython, ['Failure']); - await assert.isRejected(execution.connectToNotebookServer(undefined, true), 'Jupyter notebook failed to launch. \r\nError: The Jupyter notebook server failed to launch in time\nFailure'); - }).timeout(10000); - - test('Other than active works', async () => { - const execution = createExecution(missingNotebookPython); - await assert.eventually.equal(execution.isNotebookSupported(), true, 'Notebook not supported'); - await assert.eventually.equal(execution.isImportSupported(), true, 'Import not supported'); - await assert.eventually.equal(execution.isKernelSpecSupported(), true, 'Kernel Spec not supported'); - await assert.eventually.equal(execution.isKernelCreateSupported(), true, 'Kernel Create not supported'); - const usableInterpreter = await execution.getUsableJupyterPython(); - assert.isOk(usableInterpreter, 'Usable intepreter not found'); - if (usableInterpreter) { - assert.notEqual(usableInterpreter.path, missingNotebookPython.path); - } - }).timeout(10000); - - test('Missing kernel python still finds interpreter', async () => { - const execution = createExecution(missingKernelPython); - when(interpreterService.getActiveInterpreter()).thenResolve(missingKernelPython); - await assert.eventually.equal(execution.isNotebookSupported(), true, 'Notebook not supported'); - const usableInterpreter = await execution.getUsableJupyterPython(); - assert.isOk(usableInterpreter, 'Usable intepreter 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('Other than active finds closest match', async () => { - const execution = createExecution(missingNotebookPython); - when(interpreterService.getActiveInterpreter()).thenResolve(missingNotebookPython); - await assert.eventually.equal(execution.isNotebookSupported(), true, 'Notebook not supported'); - const usableInterpreter = await execution.getUsableJupyterPython(); - assert.isOk(usableInterpreter, 'Usable intepreter not found'); - if (usableInterpreter) { // Linter - assert.notEqual(usableInterpreter.path, missingNotebookPython.path); - assert.notEqual(usableInterpreter.version!.major, missingNotebookPython.version!.major, 'Found interpreter should not match on major'); - } - // Force config change and ask again - pythonSettings.datascience.searchForJupyter = false; - const evt = { - affectsConfiguration(m: string) : boolean { - return true; - } - }; - configChangeEvent.fire(evt); - await assert.eventually.equal(execution.isNotebookSupported(), false, 'Notebook should not be supported after config change'); - }).timeout(10000); - - test('Kernelspec is deleted on exit', async () => { - const execution = createExecution(missingKernelPython); - await assert.isFulfilled(execution.connectToNotebookServer(undefined, true), 'Should be able to start a server'); - await cleanupDisposables(); - const exists = fs.existsSync(workingKernelSpec); - assert.notOk(exists, 'Temp kernel spec still exists'); - }).timeout(10000); - - test('Jupyter found on the path', async () => { - // Make sure we can find jupyter on the path if we - // can't find it in a python module. - const execution = createExecution(missingNotebookPython); - when(interpreterService.getInterpreters()).thenResolve([missingNotebookPython]); - when(fileSystem.getFiles(anyString())).thenResolve([jupyterOnPath]); - await assert.isFulfilled(execution.connectToNotebookServer(undefined, true), 'Should be able to start a server'); - }).timeout(10000); -}); diff --git a/src/test/datascience/executionServiceMock.ts b/src/test/datascience/executionServiceMock.ts deleted file mode 100644 index aefb095b50cb..000000000000 --- a/src/test/datascience/executionServiceMock.ts +++ /dev/null @@ -1,70 +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, - InterpreterInfomation, - IPythonExecutionService, - ObservableExecutionResult, - SpawnOptions -} from '../../client/common/process/types'; -import { Architecture } from '../../client/common/utils/platform'; - -export class MockPythonExecutionService implements IPythonExecutionService { - - private procService : ProcessService; - private pythonPath : string = 'python'; - - constructor() { - this.procService = new ProcessService(new BufferDecoder()); - } - public getInterpreterInformation(): Promise<InterpreterInfomation> { - 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; - } -} 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/history.functional.test.tsx b/src/test/datascience/history.functional.test.tsx deleted file mode 100644 index d970684d596f..000000000000 --- a/src/test/datascience/history.functional.test.tsx +++ /dev/null @@ -1,546 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -//tslint:disable:trailing-comma no-any no-multiline-string -import * as assert from 'assert'; -import { mount, ReactWrapper } from 'enzyme'; -import * as fs from 'fs-extra'; -import { min } from 'lodash'; -import * as path from 'path'; -import * as React from 'react'; -import { SemVer } from 'semver'; -import * as TypeMoq from 'typemoq'; -import { CancellationToken, Disposable, TextDocument, TextEditor } from 'vscode'; - -import { - IApplicationShell, - IDocumentManager, - IWebPanel, - IWebPanelMessageListener, - IWebPanelProvider, - WebPanelMessage, -} from '../../client/common/application/types'; -import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import { createDeferred, Deferred } from '../../client/common/utils/async'; -import { noop } from '../../client/common/utils/misc'; -import { Architecture } from '../../client/common/utils/platform'; -import { EditorContexts, HistoryMessages } from '../../client/datascience/constants'; -import { IHistoryProvider, IJupyterExecution } from '../../client/datascience/types'; -import { InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; -import { Cell } from '../../datascience-ui/history-react/cell'; -import { CellButton } from '../../datascience-ui/history-react/cellButton'; -import { MainPanel } from '../../datascience-ui/history-react/MainPanel'; -import { IVsCodeApi } from '../../datascience-ui/react-common/postOffice'; -import { sleep } from '../core'; -import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { SupportedCommands } from './mockJupyterManager'; -import { waitForUpdate } from './reactHelpers'; - -// tslint:disable-next-line:max-func-body-length no-any -suite('History output tests', () => { - const disposables: Disposable[] = []; - let jupyterExecution: IJupyterExecution; - let webPanelProvider : TypeMoq.IMock<IWebPanelProvider>; - let webPanel : TypeMoq.IMock<IWebPanel>; - let historyProvider : IHistoryProvider; - let webPanelListener : IWebPanelMessageListener; - let globalAcquireVsCodeApi : () => IVsCodeApi; - let ioc: DataScienceIocContainer; - let webPanelMessagePromise: Deferred<void> | undefined; - - 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(() => { - ioc = new DataScienceIocContainer(); - ioc.registerDataScienceTypes(); - - if (ioc.mockJupyter) { - ioc.mockJupyter.addInterpreter(workingPython, SupportedCommands.all); - } - - webPanelProvider = TypeMoq.Mock.ofType<IWebPanelProvider>(); - webPanel = TypeMoq.Mock.ofType<IWebPanel>(); - - ioc.serviceManager.addSingletonInstance<IWebPanelProvider>(IWebPanelProvider, webPanelProvider.object); - - // Setup the webpanel provider so that it returns our dummy web panel. It will have to talk to our global JSDOM window so that the react components can link into it - webPanelProvider.setup(p => p.create(TypeMoq.It.isAny(), TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns((listener : IWebPanelMessageListener, title: string, script: string, css: string) => { - // Keep track of the current listener. It listens to messages through the vscode api - webPanelListener = listener; - - // Return our dummy web panel - return webPanel.object; - }); - webPanel.setup(p => p.postMessage(TypeMoq.It.isAny())).callback((m : WebPanelMessage) => { - window.postMessage(m, '*'); - }); // See JSDOM valid target origins - webPanel.setup(p => p.show()); - - jupyterExecution = ioc.serviceManager.get<IJupyterExecution>(IJupyterExecution); - historyProvider = ioc.serviceManager.get<IHistoryProvider>(IHistoryProvider); - - // Setup a global for the acquireVsCodeApi so that the React PostOffice can find it - globalAcquireVsCodeApi = () : IVsCodeApi => { - return { - // tslint:disable-next-line:no-any - postMessage: (msg: any) => { - if (webPanelListener) { - webPanelListener.onMessage(msg.type, msg.payload); - } - if (webPanelMessagePromise) { - webPanelMessagePromise.resolve(); - } - }, - // 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['acquireVsCodeApi'] = globalAcquireVsCodeApi; - }); - - teardown(async () => { - for (let i = 0; i < disposables.length; i += 1) { - const disposable = disposables[i]; - if (disposable) { - // tslint:disable-next-line:no-any - const promise = disposable.dispose() as Promise<any>; - if (promise) { - await promise; - } - } - } - await ioc.dispose(); - delete global['ascquireVsCodeApi']; - }); - - 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 addContinuousMockData(code: string, resultGenerator: (c: CancellationToken) => Promise<{result: string; haveMore: boolean}>) { - if (ioc.mockJupyter) { - ioc.mockJupyter.addContinuousOutputCell(code, resultGenerator); - } - } - - // tslint:disable-next-line:no-any - function runMountedTest(name: string, testFunc: (wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) => Promise<void>) { - test(name, async () => { - addMockData('a=1\na', 1); - if (await jupyterExecution.isNotebookSupported()) { - // Create our main panel and tie it into the JSDOM. Ignore progress so we only get a single render - const wrapper = mount(<MainPanel theme='vscode-light' ignoreProgress={true} skipDefault={true} ignoreSysInfo={true} ignoreScrolling={true} />); - try { - await testFunc(wrapper); - } finally { - // Make sure to unmount the wrapper or it will interfere with other tests - wrapper.unmount(); - } - } else { - // tslint:disable-next-line:no-console - console.log(`${name} skipped, no Jupyter installed.`); - } - }).timeout(60000); - } - - function verifyHtmlOnLastCell(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, html: string) { - const foundResult = wrapper.find('Cell'); - assert.ok(foundResult.length >= 1, 'Didn\'t find any cells being rendered'); - - // Extract only the first 100 chars from the input string - const sliced = html.substr(0, min([html.length, 100])); - - // There should be some sort of span with 1 in it - const lastCell = foundResult.last(); - assert.ok(lastCell, 'Last call doesn\'t exist'); - const output = lastCell.find('div.cell-output'); - assert.ok(output.length > 0, 'No output cell found'); - const outHtml = output.html(); - assert.ok(outHtml.includes(sliced), `${outHtml} does not contain ${sliced}`); - } - - async function waitForMessageResponse(action: () => void) : Promise<void> { - webPanelMessagePromise = createDeferred(); - action(); - await webPanelMessagePromise.promise; - webPanelMessagePromise = undefined; - } - - async function getCellResults(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, expectedRenders: number, updater: () => Promise<void>) : Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { - - // Get a render promise with the expected number of renders - const renderPromise = waitForUpdate(wrapper, MainPanel, expectedRenders); - - // Call our function to update the react control - await updater(); - - // Wait for all of the renders to go through - await renderPromise; - - // Return the result - return wrapper.find('Cell'); - } - - async function addCode(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, code: string, expectedRenderCount: number = 5): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { - // Adding code should cause 5 renders to happen. - // 1) Input - // 2) Status ready - // 3) Execute_Input message - // 4) Output message (if there's only one) - // 5) Status finished - return getCellResults(wrapper, expectedRenderCount, async () => { - const history = historyProvider.getOrCreateActive(); - await history.addCode(code, 'foo.py', 2); - }); - } - - runMountedTest('Simple text', async (wrapper) => { - await addCode(wrapper, 'a=1\na'); - - verifyHtmlOnLastCell(wrapper, '<span>1</span>'); - }); - - function escapePath(p: string) { - return p.replace(/\\/g, '\\\\'); - } - - function srcDirectory() { - return path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); - } - - runMountedTest('Mime Types', async (wrapper) => { - 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 = await fs.readFile(path.join(srcDirectory(), 'matplotlib.txt'), 'utf8'); - 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(badPanda, `pd has no attribute 'read'`, 'text/html', 'error'); - addMockData(goodPanda, `<td>A table</td>`, 'text/html'); - addMockData(matPlotLib, matPlotLibResults, 'text/html'); - const cursors = ['|', '/', '-', '\\']; - let cursorPos = 0; - let loops = 3; - addContinuousMockData(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(wrapper, badPanda, 4); - verifyHtmlOnLastCell(wrapper, `pd has no attribute 'read'`); - - await addCode(wrapper, goodPanda); - verifyHtmlOnLastCell(wrapper, `<td>`); - - await addCode(wrapper, matPlotLib); - verifyHtmlOnLastCell(wrapper, matPlotLibResults); - - await addCode(wrapper, spinningCursor, 4 + (cursors.length * 3)); - verifyHtmlOnLastCell(wrapper, '<xmp>\\'); - }); - - runMountedTest('Undo/redo commands', async (wrapper) => { - const history = historyProvider.getOrCreateActive(); - - // Get a cell into the list - await addCode(wrapper, 'a=1\na'); - - // Now verify if we undo, we have no cells - let afterUndo = await getCellResults(wrapper, 1, async () => { - await history.undoCells(); - }); - - assert.equal(afterUndo.length, 0, `Undo should remove cells + ${afterUndo.debug()}`); - - // Redo should put the cells back - const afterRedo = await getCellResults(wrapper, 1, async () => { - await history.redoCells(); - }); - assert.equal(afterRedo.length, 1, 'Redo should put cells back'); - - // Get another cell into the list - const afterAdd = await addCode(wrapper, 'a=1\na'); - assert.equal(afterAdd.length, 2, 'Second cell did not get added'); - - // Clear everything - const afterClear = await getCellResults(wrapper, 1, async () => { - await history.removeAllCells(); - }); - assert.equal(afterClear.length, 0, 'Clear didn\'t work'); - - // Undo should put them back - afterUndo = await getCellResults(wrapper, 1, async () => { - await history.undoCells(); - }); - - assert.equal(afterUndo.length, 2, `Undo should put cells back`); - }); - - function findButton(wrapper: ReactWrapper, React.Component>, index: number) : ReactWrapper, React.Component> | undefined { - const mainObj = wrapper.find(MainPanel); - if (mainObj) { - const buttons = mainObj.find(CellButton); - if (buttons) { - return buttons.at(index); - } - } - } - - runMountedTest('Click buttons', async (wrapper) => { - // 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(); - const visibleEditor = TypeMoq.Mock.ofType(); - const dummyDocument = TypeMoq.Mock.ofType(); - dummyDocument.setup(d => d.fileName).returns(() => 'foo.py'); - 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, docManager.object); - - // Get a cell into the list - await addCode(wrapper, 'a=1\na'); - - // 'Click' the buttons in the react control - const undo = findButton(wrapper, 5); - const redo = findButton(wrapper, 6); - const clear = findButton(wrapper, 7); - - // Now verify if we undo, we have no cells - let afterUndo = await getCellResults(wrapper, 1, () => { - undo.simulate('click'); - return Promise.resolve(); - }); - - assert.equal(afterUndo.length, 0, `Undo should remove cells + ${afterUndo.debug()}`); - - // Redo should put the cells back - const afterRedo = await getCellResults(wrapper, 1, async () => { - redo.simulate('click'); - return Promise.resolve(); - }); - assert.equal(afterRedo.length, 1, 'Redo should put cells back'); - - // Get another cell into the list - const afterAdd = await addCode(wrapper, 'a=1\na'); - assert.equal(afterAdd.length, 2, 'Second cell did not get added'); - - // Clear everything - const afterClear = await getCellResults(wrapper, 1, async () => { - clear.simulate('click'); - return Promise.resolve(); - }); - assert.equal(afterClear.length, 0, 'Clear didn\'t work'); - - // Undo should put them back - afterUndo = await getCellResults(wrapper, 1, async () => { - undo.simulate('click'); - return Promise.resolve(); - }); - - assert.equal(afterUndo.length, 2, `Undo should put cells back`); - - // find the buttons on the cell itself - const cellButtons = afterUndo.last().find(CellButton); - assert.equal(cellButtons.length, 2, 'Cell buttons not found'); - const goto = cellButtons.at(1); - const deleteButton = cellButtons.at(0); - - // Make sure goto works - await waitForMessageResponse(() => goto.simulate('click')); - await Promise.race([sleep(100), showedEditor.promise]); - assert.ok(showedEditor.resolved, 'Goto source is not jumping to editor'); - - // Make sure delete works - const afterDelete = await getCellResults(wrapper, 1, async () => { - deleteButton.simulate('click'); - return Promise.resolve(); - }); - assert.equal(afterDelete.length, 1, `Delete should remove a cell`); - }); - - runMountedTest('Export', async (wrapper) => { - // 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(); - appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAnyString())).returns(() => Promise.resolve('')); - 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, appShell.object); - - // Make sure to create the history after the rebind or it gets the wrong application shell. - await addCode(wrapper, 'a=1\na'); - const history = historyProvider.getOrCreateActive(); - - // Export should cause exportCalled to change to true - await waitForMessageResponse(() => history.exportCells()); - assert.equal(exportCalled, true, 'Export is not being called during export'); - - // Remove the cell - const exportButton = findButton(wrapper, 2); - const undo = findButton(wrapper, 5); - - // Now verify if we undo, we have no cells - const afterUndo = await getCellResults(wrapper, 1, () => { - undo.simulate('click'); - return Promise.resolve(); - }); - - assert.equal(afterUndo.length, 0, `Undo should remove cells + ${afterUndo.debug()}`); - - // Then verify we cannot click the button (it should be disabled) - exportCalled = false; - const response = waitForMessageResponse(() => exportButton.simulate('click')); - await Promise.race([sleep(10), response]); - assert.equal(exportCalled, false, 'Export should not be called when no cells visible'); - - }); - - test('Loc React test', async () => { - // Create our main panel and tie it into the JSDOM - const wrapper = mount(); - - // Our cell should have been rendered. It should have a method to get a loc string - const cellFound = wrapper.find('Cell'); - const cell = cellFound.at(0).instance() as Cell; - assert.equal(cell.getUnknownMimeTypeFormatString(), 'Mime type {0} is not currently supported', 'Unknown mime type did not come from script'); - }); - - test('Dispose test', async () => { - // tslint:disable-next-line:no-any - if (await jupyterExecution.isNotebookSupported()) { - const history = historyProvider.getOrCreateActive(); - await history.show(); // Have to wait for the load to finish - await history.dispose(); - // tslint:disable-next-line:no-any - const h2 = historyProvider.getOrCreateActive(); - // Check equal and then dispose so the test goes away - const equal = Object.is(history, h2); - await h2.show(); - assert.ok(!equal, 'Disposing is not removing the active history'); - } else { - // tslint:disable-next-line:no-console - console.log('History test skipped, no Jupyter installed'); - } - }); - - runMountedTest('Editor Context', async (wrapper) => { - // Verify we can send different commands to the UI and it will respond - const history = historyProvider.getOrCreateActive(); - - // 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'); - - // Get an update promise so we can wait for the add code - const updatePromise = waitForUpdate(wrapper, MainPanel); - - // Send some code to the history - await history.addCode('a=1\na', '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(); - 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(); - }; - - // Now send an undo command. This should change the state, so use our waitForInfo promise instead - resetWaiting(); - history.postMessage(HistoryMessages.Undo); - await Promise.race([deferred.promise, sleep(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(); - history.postMessage(HistoryMessages.Redo); - await Promise.race([deferred.promise, sleep(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(); - history.postMessage(HistoryMessages.DeleteAllCells); - await Promise.race([deferred.promise, sleep(2000)]); - assert.ok(deferred.resolved, 'Never got update to state'); - assert.equal(ioc.getContext(EditorContexts.HaveInteractiveCells), false, 'Should not have interactive cells after delete'); - }); - - // Tests to do: - // 1) Cell output works on different mime types. Could just use a notebook to drive - // 2) History commands work (export/restart/clear all) - // 3) Jupyter server commands work (open notebook) - // 4) Changing directories or loading from different directories - // 5) Telemetry -}); diff --git a/src/test/datascience/historyCommandListener.unit.test.ts b/src/test/datascience/historyCommandListener.unit.test.ts deleted file mode 100644 index fe76b2d71698..000000000000 --- a/src/test/datascience/historyCommandListener.unit.test.ts +++ /dev/null @@ -1,326 +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, when } from 'ts-mockito'; -import { Matcher } from 'ts-mockito/lib/matcher/type/Matcher'; -import * as TypeMoq from 'typemoq'; -import { - Disposable, - Event, - EventEmitter, - TextDocument, - TextDocumentShowOptions, - TextEditor, - TextEditorOptionsChangeEvent, - TextEditorSelectionChangeEvent, - TextEditorViewColumnChangeEvent, - Uri, - ViewColumn, - WorkspaceEdit -} from 'vscode'; - -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { IDocumentManager } from '../../client/common/application/types'; -import { PythonSettings } from '../../client/common/configSettings'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { Logger } from '../../client/common/logger'; -import { FileSystem } from '../../client/common/platform/fileSystem'; -import { IFileSystem } from '../../client/common/platform/types'; -import { IConfigurationService, ILogger } from '../../client/common/types'; -import { generateCells } from '../../client/datascience/cellFactory'; -import { Commands } from '../../client/datascience/constants'; -import { HistoryCommandListener } from '../../client/datascience/historycommandlistener'; -import { HistoryProvider } from '../../client/datascience/historyProvider'; -import { JupyterExecution } from '../../client/datascience/jupyter/jupyterExecution'; -import { JupyterExporter } from '../../client/datascience/jupyter/jupyterExporter'; -import { JupyterImporter } from '../../client/datascience/jupyter/jupyterImporter'; -import { IHistory, INotebookServer, IStatusProvider } from '../../client/datascience/types'; -import { InterpreterService } from '../../client/interpreter/interpreterService'; -import { KnownSearchPathsForInterpreters } from '../../client/interpreter/locators/services/KnownPathsService'; -import { ServiceContainer } from '../../client/ioc/container'; -import { noop } from '../core'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; -import * as vscodeMocks from '../vscode-mock'; -import { createDocument } from './editor-integration/helpers'; -import { MockCommandManager } from './mockCommandManager'; - -// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length - -function createTypeMoq(tag: string): TypeMoq.IMock { - // 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(); - result['tag'] = tag; - result.setup((x: any) => x.then).returns(() => undefined); - return result; -} - -class MockDocumentManager implements IDocumentManager { - public textDocuments: TextDocument[] = []; - public activeTextEditor: TextEditor | undefined; - public visibleTextEditors: TextEditor[] = []; - private didChangeEmitter = new EventEmitter(); - private didOpenEmitter = new EventEmitter(); - private didChangeVisibleEmitter = new EventEmitter(); - private didChangeTextEditorSelectionEmitter = new EventEmitter(); - private didChangeTextEditorOptionsEmitter = new EventEmitter(); - private didChangeTextEditorViewColumnEmitter = new EventEmitter(); - private didCloseEmitter = new EventEmitter(); - private didSaveEmitter = new EventEmitter(); - public get onDidChangeActiveTextEditor(): Event { - return this.didChangeEmitter.event; - } - public get onDidOpenTextDocument(): Event { - return this.didOpenEmitter.event; - } - public get onDidChangeVisibleTextEditors(): Event { - return this.didChangeVisibleEmitter.event; - } - public get onDidChangeTextEditorSelection(): Event { - return this.didChangeTextEditorSelectionEmitter.event; - } - public get onDidChangeTextEditorOptions(): Event { - return this.didChangeTextEditorOptionsEmitter.event; - } - public get onDidChangeTextEditorViewColumn(): Event { - return this.didChangeTextEditorViewColumnEmitter.event; - } - public get onDidCloseTextDocument(): Event { - return this.didCloseEmitter.event; - } - public get onDidSaveTextDocument(): Event { - return this.didSaveEmitter.event; - } - public showTextDocument(document: TextDocument, column?: ViewColumn, preserveFocus?: boolean): Thenable; - public showTextDocument(document: TextDocument | Uri, options?: TextDocumentShowOptions): Thenable; - public showTextDocument(document: any, column?: any, preserveFocus?: any): Thenable { - const mockEditor = createTypeMoq('TextEditor'); - mockEditor.setup(e => e.document).returns(() => this.getDocument()); - this.activeTextEditor = mockEditor.object; - return Promise.resolve(mockEditor.object); - } - public openTextDocument(fileName: string | Uri): Thenable; - public openTextDocument(options?: { language?: string; content?: string }): Thenable; - public openTextDocument(options?: any): Thenable { - return Promise.resolve(this.getDocument()); - } - public applyEdit(edit: WorkspaceEdit): Thenable { - throw new Error('Method not implemented.'); - } - - private getDocument(): TextDocument { - const mockDoc = createDocument('#%%\r\nprint("code")', 'bar.ipynb', 1, TypeMoq.Times.atMost(100), true); - mockDoc.setup((x: any) => x.then).returns(() => undefined); - return mockDoc.object; - } -} - -class MockStatusProvider implements IStatusProvider { - public set(message: string, timeout?: number): Disposable { - return { - dispose: noop - }; - } - - public waitWithStatus(promise: () => Promise, message: string, timeout?: number, canceled?: () => void): Promise { - return promise(); - } - -} - -// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length -suite('History command listener', async () => { - const interpreterService = mock(InterpreterService); - const configService = mock(ConfigurationService); - const knownSearchPaths = mock(KnownSearchPathsForInterpreters); - const logger = mock(Logger); - const fileSystem = mock(FileSystem); - const serviceContainer = mock(ServiceContainer); - const dummyEvent = new EventEmitter(); - const pythonSettings = new PythonSettings(undefined, new MockAutoSelectionService()); - const disposableRegistry = []; - const historyProvider = mock(HistoryProvider); - const notebookImporter = mock(JupyterImporter); - const notebookExporter = mock(JupyterExporter); - const applicationShell = mock(ApplicationShell); - const jupyterExecution = mock(JupyterExecution); - const documentManager = new MockDocumentManager(); - const statusProvider = new MockStatusProvider(); - const commandManager = new MockCommandManager(); - const server = createTypeMoq('jupyter server'); - let lastFileContents: any; - - suiteSetup(() => { - vscodeMocks.initialize(); - }); - suiteTeardown(() => { - noop(); - }); - - setup(() => { - noop(); - }); - - 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(activeHistory: IHistory | undefined): HistoryCommandListener { - // Setup defaults - when(interpreterService.onDidChangeInterpreter).thenReturn(dummyEvent.event); - when(interpreterService.getInterpreterDetails(argThat(o => !o.includes || !o.includes('python')))).thenReject('Unknown interpreter'); - - // Service container needs logger, file system, and config service - when(serviceContainer.get(IConfigurationService)).thenReturn(instance(configService)); - when(serviceContainer.get(IFileSystem)).thenReturn(instance(fileSystem)); - when(serviceContainer.get(ILogger)).thenReturn(instance(logger)); - when(configService.getSettings()).thenReturn(pythonSettings); - - // Setup default settings - pythonSettings.datascience = { - allowImportFromNotebook: true, - jupyterLaunchTimeout: 10, - enabled: true, - jupyterServerURI: '', - changeDirOnImportExport: true, - notebookFileRoot: 'WORKSPACE', - useDefaultConfigForJupyter: true, - jupyterInterruptTimeout: 10000, - searchForJupyter: true - }; - - when(knownSearchPaths.getSearchPaths()).thenReturn(['/foo/bar']); - - // We also need a file system - const tempFile = { - dispose: () => { - return undefined; - }, - filePath: '/foo/bar/baz.py' - }; - when(fileSystem.createTemporaryFile(anything())).thenResolve(tempFile); - when(fileSystem.deleteDirectory(anything())).thenResolve(); - when(fileSystem.writeFile(anything(), argThat(o => { lastFileContents = o; return true; }))).thenResolve(); - when(fileSystem.arePathsSame(anything(), anything())).thenReturn(true); - - when(historyProvider.getActive()).thenReturn(activeHistory); - 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 - } - ); - - if (jupyterExecution.isNotebookSupported) { - when(jupyterExecution.isNotebookSupported()).thenResolve(true); - } - - const result = new HistoryCommandListener( - disposableRegistry, - instance(historyProvider), - instance(notebookImporter), - instance(notebookExporter), - instance(jupyterExecution), - documentManager, - instance(applicationShell), - instance(fileSystem), - instance(logger), - instance(configService), - statusProvider); - - result.register(commandManager); - - return result; - } - - test('Import', async () => { - createCommandListener(undefined); - when(applicationShell.showOpenDialog(argThat(o => o.openLabel && o.openLabel.includes('Import')))).thenReturn(Promise.resolve([Uri.file('foo')])); - await commandManager.executeCommand(Commands.ImportNotebook); - assert.ok(documentManager.activeTextEditor, 'Imported file was not opened'); - }); - test('Import File', async () => { - createCommandListener(undefined); - await commandManager.executeCommand(Commands.ImportNotebook, Uri.file('bar.ipynb')); - assert.ok(documentManager.activeTextEditor, 'Imported file was not opened'); - }); - test('Export File', async () => { - createCommandListener(undefined); - 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, Uri.file('bar.ipynb')); - assert.ok(lastFileContents, 'Export file was not written to'); - }); - test('Export File and output', async () => { - createCommandListener(undefined); - const doc = await documentManager.openTextDocument('bar.ipynb'); - await documentManager.showTextDocument(doc); - when(jupyterExecution.connectToNotebookServer(anything(), anything())).thenResolve(server.object); - server.setup(s => s.execute(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAnyNumber(), TypeMoq.It.isAny())).returns(() => { - return Promise.resolve(generateCells('a=1', 'bar.py', 0, false)); - }); - - 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')); - - await commandManager.executeCommand(Commands.ExportFileAndOutputAsNotebook, Uri.file('bar.ipynb')); - assert.ok(lastFileContents, 'Export file was not written to'); - }); - test('Export skipped on no file', async () => { - createCommandListener(undefined); - 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(undefined); - 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); - assert.ok(lastFileContents, 'Export file was not written to'); - }); - -}); 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 @@ - diff --git a/src/test/datascience/mockCommandManager.ts b/src/test/datascience/mockCommandManager.ts deleted file mode 100644 index 1525a37ba7d6..000000000000 --- a/src/test/datascience/mockCommandManager.ts +++ /dev/null @@ -1,39 +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 { 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: {[key: string]: (...args: any[]) => any} = {}; - - public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { - this.commands[command] = callback; - 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(command: string, ...rest: any[]): Thenable { - const func = this.commands[command]; - const result = func(...rest); - const tPromise = result as Promise; - if (tPromise) { - return tPromise; - } - return Promise.resolve(result); - } - public getCommands(filterInternal?: boolean): Thenable { - const keys = Object.keys(this.commands); - return Promise.resolve(keys); - } -} diff --git a/src/test/datascience/mockJupyterManager.ts b/src/test/datascience/mockJupyterManager.ts deleted file mode 100644 index fe4db14d4325..000000000000 --- a/src/test/datascience/mockJupyterManager.ts +++ /dev/null @@ -1,433 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { nbformat } from '@jupyterlab/coreutils'; -import * as fs from 'fs-extra'; -import * as os from 'os'; -import * as path from 'path'; -import { Observable } from 'rxjs/Observable'; -import * as TypeMoq from 'typemoq'; -import * as uuid from 'uuid/v4'; -import { EventEmitter } from 'vscode'; -import { CancellationToken } from 'vscode-jsonrpc'; - -import { Cancellation } from '../../client/common/cancellation'; -import { PythonSettings } from '../../client/common/configSettings'; -import { ExecutionResult, IProcessServiceFactory, IPythonExecutionFactory, Output } from '../../client/common/process/types'; -import { IAsyncDisposableRegistry, IConfigurationService } from '../../client/common/types'; -import { EXTENSION_ROOT_DIR } from '../../client/constants'; -import { generateCells } from '../../client/datascience/cellFactory'; -import { concatMultilineString } from '../../client/datascience/common'; -import { IConnection, IJupyterKernelSpec, IJupyterSession, IJupyterSessionManager } from '../../client/datascience/types'; -import { IInterpreterService, PythonInterpreter } from '../../client/interpreter/contracts'; -import { IServiceManager } from '../../client/ioc/types'; -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 -} - -// 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 { - private pythonExecutionFactory = this.createTypeMoq('Python Exec Factory'); - private processServiceFactory = this.createTypeMoq('Process Exec Factory'); - private processService: MockProcessService = new MockProcessService(); - private interpreterService = this.createTypeMoq('Interpreter Service'); - private asyncRegistry : IAsyncDisposableRegistry; - private changedInterpreterEvent: EventEmitter = new EventEmitter(); - private installedInterpreters : PythonInterpreter[] = []; - private pythonServices: MockPythonService[] = []; - private activeInterpreter: PythonInterpreter | undefined; - private sessionTimeout: number | undefined; - private cellDictionary = {}; - private kernelSpecs : {name: string; dir: string}[] = []; - - constructor(serviceManager: IServiceManager) { - // Save async registry. Need to stick servers created into it - this.asyncRegistry = serviceManager.get(IAsyncDisposableRegistry); - - // Make our process service factory always return this item - this.processServiceFactory.setup(p => p.create()).returns(() => Promise.resolve(this.processService)); - - // Setup our interpreter service - this.interpreterService.setup(i => i.onDidChangeInterpreter).returns(() => this.changedInterpreterEvent.event); - this.interpreterService.setup(i => i.getActiveInterpreter()).returns(() => Promise.resolve(this.activeInterpreter)); - this.interpreterService.setup(i => i.getInterpreters()).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); - if (configService && configService !== null) { - (configService.getSettings() as PythonSettings).addListener('change', this.onConfigChanged); - } - - // Stick our services into the service manager - serviceManager.addSingletonInstance(IJupyterSessionManager, this); - serviceManager.addSingletonInstance(IInterpreterService, this.interpreterService.object); - serviceManager.addSingletonInstance(IPythonExecutionFactory, this.pythonExecutionFactory.object); - serviceManager.addSingletonInstance(IProcessServiceFactory, this.processServiceFactory.object); - - // 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('%matplotlib inline\r\nimport matplotlib.pyplot as plt'); - this.addCell(`%cd "${path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience')}"`); - 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'); - } - - public makeActive(interpreter: PythonInterpreter) { - this.activeInterpreter = interpreter; - } - - public setProcessDelay(timeout: number | undefined) { - this.processService.setDelay(timeout); - this.pythonServices.forEach(p => p.setDelay(timeout)); - } - - public addInterpreter(interpreter: PythonInterpreter, supportedCommands: SupportedCommands, notebookStdErr?: string[]) { - 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.setupSupportedPythonService(pythonService, interpreter, supportedCommands, notebookStdErr); - - // Then the process calls - this.setupSupportedProcessService(interpreter, supportedCommands, notebookStdErr); - - // Default to being the new active - this.makeActive(interpreter); - } - - public addPath(jupyterPath: string, supportedCommands: SupportedCommands, notebookStdErr?: string[]) { - this.setupPathProcessService(jupyterPath, this.processService, supportedCommands, notebookStdErr); - } - - 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: [] - }; - - this.addCell(code, result); - } - - public addContinuousOutputCell(code: string, resultGenerator: (cancelToken: CancellationToken) => Promise<{result: string; haveMore: boolean}>) { - const cells = generateCells(code, 'foo.py', 1, true); - cells.forEach(c => { - const key = concatMultilineString(c.data.source).replace(LineFeedRegEx, ''); - 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['resultGenerator'] = async (t) => { - 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 addCell(code: string, result?: undefined | string | number | nbformat.IUnrecognizedOutput | nbformat.IExecuteResult | nbformat.IDisplayData | nbformat.IStream | nbformat.IError, mimeType?: string) { - const cells = generateCells(code, 'foo.py', 1, true); - cells.forEach(c => { - const key = concatMultilineString(c.data.source).replace(LineFeedRegEx, ''); - if (c.data.cell_type === 'code') { - const massagedResult = this.massageCellResult(result, mimeType); - const data: nbformat.ICodeCell = c.data as nbformat.ICodeCell; - data.outputs = [...data.outputs, massagedResult]; - 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 setWaitTime(timeout: number | undefined) { - this.sessionTimeout = timeout; - } - - public startNew(connInfo: IConnection, kernelSpec: IJupyterKernelSpec, cancelToken?: CancellationToken) : Promise { - this.asyncRegistry.push(connInfo); - if (kernelSpec) { - this.asyncRegistry.push(kernelSpec); - } - if (this.sessionTimeout && cancelToken) { - const localTimeout = this.sessionTimeout; - return Cancellation.race(async () => { - await sleep(localTimeout); - return new MockJupyterSession(this.cellDictionary, MockJupyterTimeDelay); - }, cancelToken); - } else { - return Promise.resolve(new MockJupyterSession(this.cellDictionary, MockJupyterTimeDelay)); - } - } - - public getActiveKernelSpecs(connection: IConnection) : Promise { - return Promise.resolve([]); - } - - private onConfigChanged = () => { - this.changedInterpreterEvent.fire(); - } - - 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 (typeof result === 'string') { - const data = {}; - data[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(tag: string): TypeMoq.IMock { - // 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(); - result['tag'] = tag; - result.setup((x: any) => x.then).returns(() => undefined); - return result; - } - - private setupPythonServiceExec(service: MockPythonService, module: string, args: (string | RegExp)[], result: () => Promise>) { - service.addExecModuleResult(module, args, result); - } - - private setupPythonServiceExecObservable(service: MockPythonService, module: string, args: (string | RegExp)[], stderr: string[], stdout: string[]) { - service.addExecModuleObservableResult(module, args, () => { - return { - proc: undefined, - out: new Observable>(subscriber => { - stderr.forEach(s => subscriber.next({ source: 'stderr', out: s })); - stdout.forEach(s => subscriber.next({ source: 'stderr', out: s })); - }), - dispose: () => { - noop(); - } - }; - }); - } - - private setupProcessServiceExec(service: MockProcessService, file: string, args: (string | RegExp)[], result: () => Promise>) { - 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>(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[]) { - 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)}` }); - }); - } - if ((supportedCommands & SupportedCommands.nbconvert) === SupportedCommands.nbconvert) { - this.setupPythonServiceExec(service, 'jupyter', ['nbconvert', '--version'], () => Promise.resolve({ stdout: '1.1.1.1' })); - } - 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=.*/, /.*/], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); - - } - 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'], () => { - const results = this.kernelSpecs.map(k => { - return ` ${k.name} ${k.dir}`; - }).join(os.EOL); - return Promise.resolve({stdout: results}); - }); - - } - } - - 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'], () => { - const results = this.kernelSpecs.map(k => { - return ` ${k.name} ${k.dir}`; - }).join(os.EOL); - return Promise.resolve({stdout: results}); - }); - 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: `somename ${path.dirname(spec)}` }); - }); - const getServerInfoPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', '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'], [], []); - this.setupProcessServiceExecObservable(this.processService, workingPython.path, ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/, /.*/], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); - this.setupProcessServiceExecObservable(this.processService, workingPython.path, ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); - } else if ((supportedCommands & SupportedCommands.notebook) === SupportedCommands.notebook) { - this.setupProcessServiceExec(this.processService, workingPython.path, ['-m', 'jupyter', 'kernelspec', 'list'], () => { - const results = this.kernelSpecs.map(k => { - return ` ${k.name} ${k.dir}`; - }).join(os.EOL); - return Promise.resolve({stdout: results}); - }); - const getServerInfoPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', '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'], [], []); - this.setupProcessServiceExecObservable(this.processService, workingPython.path, ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/, /.*/], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); - this.setupProcessServiceExecObservable(this.processService, workingPython.path, ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/], [], 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()' - }); - }); - } - } - - private setupPathProcessService(jupyterPath: string, service: MockProcessService, supportedCommands: SupportedCommands, notebookStdErr?: string[]) { - if ((supportedCommands & SupportedCommands.kernelspec) === SupportedCommands.kernelspec) { - this.setupProcessServiceExec(service, jupyterPath, ['kernelspec', 'list'], () => { - const results = this.kernelSpecs.map(k => { - return ` ${k.name} ${k.dir}`; - }).join(os.EOL); - return Promise.resolve({stdout: results}); - }); - this.setupProcessServiceExecObservable(service, jupyterPath, ['kernelspec', 'list'], [], []); - this.setupProcessServiceExec(service, jupyterPath, ['kernelspec', '--version'], () => Promise.resolve({ stdout: '1.1.1.1' })); - this.setupProcessServiceExec(service, 'jupyter', ['kernelspec', '--version'], () => Promise.resolve({ stdout: '1.1.1.1' })); - } else { - this.setupProcessServiceExec(service, jupyterPath, ['kernelspec', '--version'], () => Promise.reject()); - this.setupProcessServiceExec(service, 'jupyter', ['kernelspec', '--version'], () => Promise.reject()); - } - - this.setupProcessServiceExec(service, jupyterPath, ['--version'], () => Promise.resolve({ stdout: '1.1.1.1' })); - this.setupProcessServiceExec(service, 'jupyter', ['--version'], () => Promise.resolve({ stdout: '1.1.1.1' })); - - if ((supportedCommands & SupportedCommands.kernelspec) === SupportedCommands.kernelspec) { - this.setupProcessServiceExec(service, jupyterPath, ['notebook', '--version'], () => Promise.resolve({ stdout: '1.1.1.1' })); - this.setupProcessServiceExecObservable(service, jupyterPath, ['notebook', '--no-browser', /--notebook-dir=.*/, /.*/], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); - this.setupProcessServiceExec(service, 'jupyter', ['notebook', '--version'], () => Promise.resolve({ stdout: '1.1.1.1' })); - } else { - this.setupProcessServiceExec(service, 'jupyter', ['notebook', '--version'], () => Promise.reject()); - this.setupProcessServiceExec(service, jupyterPath, ['notebook', '--version'], () => Promise.reject()); - } - } -} diff --git a/src/test/datascience/mockJupyterRequest.ts b/src/test/datascience/mockJupyterRequest.ts deleted file mode 100644 index 33b73c6cd0c4..000000000000 --- a/src/test/datascience/mockJupyterRequest.ts +++ /dev/null @@ -1,209 +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 { concatMultilineString } from '../../client/datascience/common'; -import { ICell } from '../../client/datascience/types'; - -//tslint:disable:no-any -interface IMessageResult { - message: KernelMessage.IIOPubMessage; - haveMore: boolean; -} - -interface IMessageProducer { - produceNextMessage() : Promise; -} - -class SimpleMessageProducer implements IMessageProducer { - private type: string; - private result: any; - private channel: string = 'iopub'; - - constructor(type: string, result: any, channel: string = 'iopub') { - this.type = type; - this.result = result; - this.channel = channel; - } - - public produceNextMessage() : Promise { - return new Promise((resolve, reject) => { - const message = this.generateMessage(this.type, this.result, this.channel); - resolve({message: message, haveMore: false}); - }); - } - - protected generateMessage(msgType: string, result: any, channel: string = 'iopub') : KernelMessage.IIOPubMessage { - return { - channel: 'iopub', - header: { - username: 'foo', - version: '1.1', - session: '1111111111', - msg_id: '1.1', - msg_type: msgType - }, - parent_header: { - - }, - metadata: { - - }, - content: result - }; - } -} - -class OutputMessageProducer extends SimpleMessageProducer { - private output: nbformat.IOutput; - private cancelToken: CancellationToken; - - constructor(output: nbformat.IOutput, cancelToken: CancellationToken) { - super(output.output_type, output); - this.output = output; - this.cancelToken = cancelToken; - } - - public async produceNextMessage() : Promise { - // Special case the 'generator' cell that returns a function - // to generate output. - if (this.output.output_type === 'generator') { - const resultEntry = 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.generateMessage(streamResult.result.output_type, streamResult.result), - haveMore: streamResult.haveMore - }; - } - } - - return super.produceNextMessage(); - } -} - -// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length -export class MockJupyterRequest implements Kernel.IFuture { - public msg: KernelMessage.IShellMessage; - public onReply: (msg: KernelMessage.IShellMessage) => void | PromiseLike; - public onStdin: (msg: KernelMessage.IStdinMessage) => void | PromiseLike; - public onIOPub: (msg: KernelMessage.IIOPubMessage) => void | PromiseLike; - public isDisposed: boolean = false; - - private deferred: Deferred = createDeferred(); - private executionCount: number; - private cell: ICell; - private cancelToken: CancellationToken; - - 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' - }, - 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 { - return this.deferred.promise; - } - public registerMessageHook(hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike): void { - noop(); - } - public removeMessageHook(hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike): void { - noop(); - } - public sendInputReply(content: KernelMessage.IInputReply): void { - noop(); - } - 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, this.cancelToken)); - - // Then combine those into an array of producers for the rest of the messages - const producers = [ - new SimpleMessageProducer('status', { execution_state: 'busy'}), - new SimpleMessageProducer('execute_input', { code: concatMultilineString(this.cell.data.source), execution_count: this.executionCount }), - ...outputProducers, - new SimpleMessageProducer('status', { execution_state: 'idle'}) - ]; - - // Then send these until we're done - this.sendMessages(producers, delay); - } - - private sendMessages(producers: IMessageProducer[], delay: number) { - if (producers && producers.length > 0) { - // We have another producer, after a delay produce the next - // message - const producer = producers[0]; - setTimeout(() => { - // Produce the next message - producer.produceNextMessage().then(r => { - // If there's a message, send it. - if (r.message && this.onIOPub) { - this.onIOPub(r.message); - } - - // Move onto the next producer if allowed - if (r.haveMore) { - this.sendMessages(producers, delay); - } else { - this.sendMessages(producers.slice(1), delay); - } - }).ignoreErrors(); - }, delay); - } else { - // No more messages, create a simple producer for our shell message - const shellProducer = new SimpleMessageProducer('done', {status: 'success'}, 'shell'); - shellProducer.produceNextMessage().then((r) => { - this.deferred.resolve(r.message as KernelMessage.IShellMessage); - }).ignoreErrors(); - } - } -} diff --git a/src/test/datascience/mockJupyterSession.ts b/src/test/datascience/mockJupyterSession.ts deleted file mode 100644 index 37bf059cdbe5..000000000000 --- a/src/test/datascience/mockJupyterSession.ts +++ /dev/null @@ -1,78 +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 { ICell, IJupyterSession } from '../../client/datascience/types'; -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 { - private dict: {[index: string] : ICell}; - private restartedEvent: EventEmitter = new EventEmitter(); - private timedelay: number; - private executionCount: number = 0; - private outstandingRequestTokenSources: CancellationTokenSource[] = []; - - constructor(cellDictionary: {[index: string] : ICell}, timedelay: number) { - this.dict = cellDictionary; - this.timedelay = timedelay; - } - - public get onRestarted() : Event { - return this.restartedEvent.event; - } - - public async restart(): Promise { - // For every outstanding request, switch them to fail mode - const requests = [...this.outstandingRequestTokenSources]; - requests.forEach(r => r.cancel()); - return sleep(this.timedelay); - } - public interrupt(): Promise { - const requests = [...this.outstandingRequestTokenSources]; - requests.forEach(r => r.cancel()); - return sleep(this.timedelay); - } - public waitForIdle(): Promise { - return sleep(this.timedelay); - } - public requestExecute(content: KernelMessage.IExecuteRequest, disposeOnDone?: boolean, metadata?: JSONObject): Kernel.IFuture { - // Content should have the code - const cell = this.findCell(content.code); - - // Create a new dummy request - this.executionCount += 1; - 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); - }; - request.done.then(removeHandler).catch(removeHandler); - return request; - } - - public async dispose(): Promise { - await sleep(10); - } - - private findCell = (code : string) : ICell => { - // Match skipping line separators - const withoutLines = code.replace(LineFeedRegEx, ''); - - if (this.dict.hasOwnProperty(withoutLines)) { - return this.dict[withoutLines] as ICell; - } - // tslint:disable-next-line:no-console - console.log(`Cell ${code.splitLines()[0]} not found in mock`); - throw new Error(`Cell ${code.splitLines()[0]} not found in mock`); - } -} diff --git a/src/test/datascience/mockProcessService.ts b/src/test/datascience/mockProcessService.ts deleted file mode 100644 index 1a23b102512a..000000000000 --- a/src/test/datascience/mockProcessService.ts +++ /dev/null @@ -1,91 +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> }[] = []; - private execObservableResults: {file: string; args: (string | RegExp)[]; result(): ObservableExecutionResult }[] = []; - private timeDelay: number | undefined; - - public execObservable(file: string, args: string[], options: SpawnOptions): ObservableExecutionResult { - 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> { - 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> { - // Not supported - return this.defaultExecutionResult([command]); - } - - public addExecResult(file: string, args: (string | RegExp)[], result: () => Promise>) { - this.execResults.push({file: file, args: args, result: result}); - } - - public addExecObservableResult(file: string, args: (string | RegExp)[], result: () => ObservableExecutionResult) { - this.execObservableResults.push({file: file, args: args, result: result}); - } - - public setDelay(timeout: number | undefined) { - this.timeDelay = timeout; - } - - 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 { - const output = new Observable>(subscriber => { subscriber.next({out: `Invalid call to ${args.join(' ')}`, source: 'stderr'}); }); - return { - proc: undefined, - out: output, - dispose: () => noop - }; - } - - private defaultExecutionResult(args: string[]) : Promise> { - return Promise.resolve({stderr: `Invalid call to ${args.join(' ')}`, stdout: ''}); - } - -} diff --git a/src/test/datascience/mockPythonService.ts b/src/test/datascience/mockPythonService.ts deleted file mode 100644 index aef4d9ced543..000000000000 --- a/src/test/datascience/mockPythonService.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { - ExecutionResult, - InterpreterInfomation, - IPythonExecutionService, - ObservableExecutionResult, - SpawnOptions -} from '../../client/common/process/types'; -import { PythonInterpreter } from '../../client/interpreter/contracts'; -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 { - return Promise.resolve(this.interpreter); - } - - public getExecutablePath(): Promise { - return Promise.resolve(this.interpreter.path); - } - - public isModuleInstalled(moduleName: string): Promise { - return Promise.resolve(false); - } - - public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult { - return this.procService.execObservable(this.interpreter.path, args, options); - } - public execModuleObservable(moduleName: string, args: string[], options: SpawnOptions): ObservableExecutionResult { - return this.procService.execObservable(this.interpreter.path, ['-m', moduleName, ...args], options); - } - public exec(args: string[], options: SpawnOptions): Promise> { - return this.procService.exec(this.interpreter.path, args, options); - } - - public execModule(moduleName: string, args: string[], options: SpawnOptions): Promise> { - return this.procService.exec(this.interpreter.path, ['-m', moduleName, ...args], options); - } - - public addExecResult(args: (string | RegExp)[], result: () => Promise>) { - this.procService.addExecResult(this.interpreter.path, args, result); - } - - public addExecModuleResult(moduleName: string, args: (string | RegExp)[], result: () => Promise>) { - this.procService.addExecResult(this.interpreter.path, ['-m', moduleName, ...args], result); - } - - public addExecObservableResult(args: (string | RegExp)[], result: () => ObservableExecutionResult) { - this.procService.addExecObservableResult(this.interpreter.path, args, result); - } - - public addExecModuleObservableResult(moduleName: string, args: (string | RegExp)[], result: () => ObservableExecutionResult) { - this.procService.addExecObservableResult(this.interpreter.path, ['-m', moduleName, ...args], result); - } - - public setDelay(timeout: number | undefined) { - this.procService.setDelay(timeout); - } -} diff --git a/src/test/datascience/notebook.functional.test.ts b/src/test/datascience/notebook.functional.test.ts deleted file mode 100644 index df3b8b8c9533..000000000000 --- a/src/test/datascience/notebook.functional.test.ts +++ /dev/null @@ -1,764 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -// tslint:disable:no-any no-multiline-string max-func-body-length no-console max-classes-per-file trailing-comma -import { nbformat } from '@jupyterlab/coreutils'; -import { assert } from 'chai'; -import * as fs from 'fs-extra'; -import * as os from 'os'; -import * as path from 'path'; -import { SemVer } from 'semver'; -import { Disposable, Uri } from 'vscode'; -import { CancellationToken, CancellationTokenSource } from 'vscode-jsonrpc'; - -import { Cancellation, CancellationError } from '../../client/common/cancellation'; -import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import { IFileSystem } from '../../client/common/platform/types'; -import { IProcessServiceFactory, Output } from '../../client/common/process/types'; -import { createDeferred } from '../../client/common/utils/async'; -import { noop } from '../../client/common/utils/misc'; -import { Architecture } from '../../client/common/utils/platform'; -import { concatMultilineString } from '../../client/datascience/common'; -import { JupyterExecution } from '../../client/datascience/jupyter/jupyterExecution'; -import { - CellState, - ICell, - IConnection, - IJupyterExecution, - IJupyterKernelSpec, - INotebookExporter, - INotebookImporter, - INotebookServer, - InterruptResult, -} from '../../client/datascience/types'; -import { - IInterpreterService, - IKnownSearchPathsForInterpreters, - InterpreterType, - PythonInterpreter, -} from '../../client/interpreter/contracts'; -import { ICellViewModel } from '../../datascience-ui/history-react/cell'; -import { generateTestState } from '../../datascience-ui/history-react/mainPanelState'; -import { sleep } from '../core'; -import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { SupportedCommands } from './mockJupyterManager'; - -// tslint:disable:no-any no-multiline-string max-func-body-length no-console max-classes-per-file -suite('Jupyter notebook tests', () => { - const disposables: Disposable[] = []; - let jupyterExecution: IJupyterExecution; - let processFactory: IProcessServiceFactory; - let ioc: DataScienceIocContainer; - let modifiedConfig = false; - - 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(() => { - ioc = new DataScienceIocContainer(); - ioc.registerDataScienceTypes(); - jupyterExecution = ioc.serviceManager.get(IJupyterExecution); - processFactory = ioc.serviceManager.get(IProcessServiceFactory); - if (ioc.mockJupyter) { - ioc.mockJupyter.addInterpreter(workingPython, SupportedCommands.all); - } - }); - - teardown(async () => { - if (modifiedConfig) { - const python = await getNotebookCapableInterpreter(); - const procService = await processFactory.create(); - if (procService && python) { - await procService.exec(python.path, ['-m', 'jupyter', 'notebook', '--generate-config', '-y'], { env: process.env }); - } - } - for (let i = 0; i < disposables.length; i += 1) { - const disposable = disposables[i]; - if (disposable) { - const promise = disposable.dispose() as Promise; - if (promise) { - await promise; - } - } - } - await ioc.dispose(); - }); - - function escapePath(p: string) { - return p.replace(/\\/g, '\\\\'); - } - - function srcDirectory() { - return path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); - } - - async function verifySimple(jupyterServer: INotebookServer | undefined, code: string, expectedValue: any) : Promise { - const cells = await jupyterServer!.execute(code, path.join(srcDirectory(), 'foo.py'), 2); - 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 data = cell.outputs[0].data; - const error = cell.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['text/plain'], `Cell mime type not correct`); - assert.equal(data['text/plain'], expectedValue, 'Cell value does not match'); - } - } - - async function verifyError(jupyterServer: INotebookServer | undefined, code: string, errorString: string): Promise { - const cells = await jupyterServer!.execute(code, path.join(srcDirectory(), 'foo.py'), 2); - 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(jupyterServer: INotebookServer | undefined, index: number, code: string, mimeType: string, cellType: string, verifyValue: (data: any) => void): Promise { - // Verify results of an execute - const cells = await jupyterServer!.execute(code, path.join(srcDirectory(), 'foo.py'), 2); - 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.equal(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; - assert.ok(data, `${index}: No data object on the cell`); - if (data) { // For linter - assert.ok(data.hasOwnProperty(mimeType), `${index}: Cell mime type not correct`); - assert.ok(data[mimeType], `${index}: Cell mime type not correct`); - verifyValue(data[mimeType]); - } - } else if (cellType === 'markdown') { - assert.equal(cells[0].data.cell_type, cellType, `${index}: Wrong type of cell returned`); - const cell = cells[0].data as nbformat.IMarkdownCell; - const outputSource = concatMultilineString(cell.source); - verifyValue(outputSource); - } else if (cellType === 'error') { - const cell = cells[0].data as nbformat.ICodeCell; - assert.equal(cell.outputs.length, 1, `${index}: Cell length not correct`); - const error = cell.outputs[0].evalue; - assert.ok(error, 'Error not found when expected'); - verifyValue(error); - } - } - - function testMimeTypes(types : {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 createNotebookServer(true); - let statusCount: number = 0; - if (server) { - server.onStatusChanged((bool: boolean) => { - statusCount += 1; - }); - for (let i = 0; i < types.length; i += 1) { - const prevCount = statusCount; - await verifyCell(server, i, types[i].code, types[i].mimeType, types[i].cellType, types[i].verifyValue); - if (types[i].cellType !== 'markdown') { - assert.ok(statusCount > prevCount, 'Status didnt update'); - } - } - } - }); - } - - function runTest(name: string, func: () => Promise) { - test(name, async () => { - console.log(`Starting test ${name} ...`); - if (await jupyterExecution.isNotebookSupported()) { - return func(); - } else { - // tslint:disable-next-line:no-console - console.log(`Skipping test ${name}, no jupyter installed.`); - } - }); - } - - async function createNotebookServer(useDefaultConfig: boolean, expectFailure?: boolean) : Promise { - // Catch exceptions. Throw a specific assertion if the promise fails - try { - const testDir = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); - const server = await jupyterExecution.connectToNotebookServer(undefined, useDefaultConfig, undefined, testDir); - if (expectFailure) { - assert.ok(false, `Expected server to not be created`); - } - return server; - } 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 addInterruptableMockData(code: string, resultGenerator: (c: CancellationToken) => Promise<{result: string; haveMore: boolean}>) { - if (ioc.mockJupyter) { - ioc.mockJupyter.addContinuousOutputCell(code, resultGenerator); - } - } - - runTest('Remote', async () => { - const python = await getNotebookCapableInterpreter(); - const procService = await processFactory.create(); - - if (procService && python) { - const connectionFound = createDeferred(); - const exeResult = procService.execObservable(python.path, ['-m', 'jupyter', 'notebook', '--no-browser'], { env: process.env, throwOnStdErr: false }); - disposables.push(exeResult); - - exeResult.out.subscribe((output: Output) => { - const connectionURL = getConnectionInfo(output.out); - if (connectionURL) { - connectionFound.resolve(connectionURL); - } - }); - - const connString = await connectionFound.promise; - const uri = connString as string; - - // We have a connection string here, so try to connect jupyterExecution to the notebook server - const server = await jupyterExecution.connectToNotebookServer(uri!, true); - if (!server) { - assert.fail('Failed to connect to remote server'); - } - // Have to dispose here otherwise the process may exit before hand and mess up cleanup. - await server!.dispose(); - } - }); - - runTest('Creation', async () => { - await createNotebookServer(true); - }); - - function getConnectionInfo(output: string): string | undefined { - const UrlPatternRegEx = /(https?:\/\/[^\s]+)/; - - const urlMatch = UrlPatternRegEx.exec(output); - if (urlMatch) { - return urlMatch[0]; - } - return undefined; - } - - runTest('Failure', async () => { - // Make a dummy class that will fail during launch - class FailedProcess extends JupyterExecution { - public isNotebookSupported = (): Promise => { - return Promise.resolve(false); - } - } - ioc.serviceManager.rebind(IJupyterExecution, FailedProcess); - jupyterExecution = ioc.serviceManager.get(IJupyterExecution); - await createNotebookServer(true, true); - }); - - test('Not installed', async () => { - // Rewire our data we use to search for processes - class EmptyInterpreterService implements IInterpreterService { - public get hasInterpreters(): Promise { - return Promise.resolve(true); - } - public onDidChangeInterpreter(_listener: (e: void) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { - return { dispose: noop }; - } - public getInterpreters(resource?: Uri): Promise { - return Promise.resolve([]); - } - public autoSetInterpreter(): Promise { - throw new Error('Method not implemented'); - } - public getActiveInterpreter(resource?: Uri): Promise { - return Promise.resolve(undefined); - } - public getInterpreterDetails(pythonPath: string, resoure?: Uri): Promise { - throw new Error('Method not implemented'); - } - public refresh(resource: Uri): Promise { - throw new Error('Method not implemented'); - } - public initialize(): void { - throw new Error('Method not implemented'); - } - public getDisplayName(interpreter: Partial): Promise { - throw new Error('Method not implemented'); - } - public shouldAutoSetInterpreter(): Promise { - throw new Error('Method not implemented'); - } - } - class EmptyPathService implements IKnownSearchPathsForInterpreters { - public getSearchPaths(): string[] { - return []; - } - } - ioc.serviceManager.rebind(IInterpreterService, EmptyInterpreterService); - ioc.serviceManager.rebind(IKnownSearchPathsForInterpreters, EmptyPathService); - jupyterExecution = ioc.serviceManager.get(IJupyterExecution); - await createNotebookServer(true, true); - }); - - 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(id => { return; }, testFolderPath); - const cells = testState.cellVMs.map((cellVM: ICellViewModel, index: number) => { return cellVM.cell; }); - - // Translate this into a notebook - const exporter = ioc.serviceManager.get(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')); - } - } - - // Save to a temp file - const fileSystem = ioc.serviceManager.get(IFileSystem); - const importer = ioc.serviceManager.get(INotebookImporter); - const temp = await fileSystem.createTemporaryFile('.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(temp.filePath); - - // Make sure we added a chdir into our results - assert.ok(results.includes('os.chdir')); - - // Make sure we have a cell in our results - assert.ok(/#\s*%%/.test(results), 'No cells in returned import'); - } finally { - importer.dispose(); - temp.dispose(); - } - }); - - 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 createNotebookServer(true); - - // 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(); - - console.log('Restarting kernel'); - - await server!.restartKernel(); - - console.log('Waiting for idle'); - await server!.waitForIdle(); - - console.log('Verifying restart'); - await verifyError(server, 'a', `name 'a' is not defined`); - }); - - class TaggedCancellationTokenSource extends CancellationTokenSource { - public tag: string; - constructor(tag: string) { - super(); - this.tag = tag; - } - } - - async function testCancelableCall(method: (t: CancellationToken) => Promise, messageFormat: string, timeout: number): Promise { - const tokenSource = new TaggedCancellationTokenSource(messageFormat.format(timeout.toString())); - const disp = setTimeout((s) => { - tokenSource.cancel(); - }, timeout, tokenSource.tag); - - try { - tokenSource.token['tag'] = messageFormat.format(timeout.toString()); - await method(tokenSource.token); - assert.ok(false, messageFormat.format(timeout.toString())); - } 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(method: (t: CancellationToken) => Promise, messageFormat: string, short?: boolean): Promise { - const timeouts = short ? [10, 20, 30, 100] : [100, 200, 300, 1000]; - for (let i = 0; i < timeouts.length; i += 1) { - await testCancelableCall(method, messageFormat, timeouts[i]); - } - - return true; - } - - runTest('Cancel execution', async () => { - if (ioc.mockJupyter) { - ioc.mockJupyter.setProcessDelay(2000); - addMockData(`a=1${os.EOL}a`, 1); - } - - // Try different timeouts, canceling after the timeout on each - assert.ok(await testCancelableMethod((t: CancellationToken) => jupyterExecution.connectToNotebookServer(undefined, true, 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, true, nonCancelSource.token); - assert.ok(server, 'Server not found with a cancel token that does not cancel'); - - // Make sure can run some code too - await verifySimple(server, `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(); - - 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.isKernelCreateSupported(t), 'Cancel did not cancel isKernel 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(server: INotebookServer | undefined, code: string, interruptMs: number, sleepMs: number) : Promise { - let interrupted = false; - let finishedBefore = false; - const finishedPromise = createDeferred(); - let error; - const observable = server!.executeObservable(code, 'foo.py', 0); - let cells: ICell[] = []; - observable.subscribe(c => { - cells = 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 server!.interruptKernel(interruptMs); - - // Then we should get our finish unless there was a restart - await Promise.race([finishedPromise.promise, sleep(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.Restarted, - `Timed out before interrupt for result: ${result}: ${code}`); - - return result; - } - - runTest('Interrupt kernel', async () => { - 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 createNotebookServer(true); - - // 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 - let interruptResult = 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. - interruptResult = await interruptExecute(server, fourSecondSleep, 7000, 7000); - - // Try again with something that doesn't return. Make sure it times out - interruptResult = await interruptExecute(server, fourSecondSleep, 100, 7000); - assert.equal(interruptResult, InterruptResult.TimedOut); - - // The tough one, somethign that causes a kernel reset. - interruptResult = await interruptExecute(server, kill, 1000, 1000); - }); - - testMimeTypes( - [ - { - code: - `a=1 -a`, - mimeType: 'text/plain', - cellType: 'code', - result: 1, - verifyValue: (d) => assert.equal(d, 1, 'Plain text invalid') - }, - { - 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') - }, - { - code: - `import pandas as pd -df = pd.read_csv("${escapePath(path.join(srcDirectory(), 'DefaultSalesReport.csv'))}") -df.head()`, - mimeType: 'text/html', - result: `A table`, - cellType: 'code', - verifyValue: (d) => assert.ok(d.toString().includes(''), 'Table not found') - }, - { - code: - `#%% [markdown]# -# #HEADER`, - mimeType: 'text/plain', - cellType: 'markdown', - result: '#HEADER', - verifyValue: (d) => assert.equal(d, '#HEADER', 'Markdown incorrect') - }, - { - // Test relative directories too. - code: - `import pandas as pd -df = pd.read_csv("./DefaultSalesReport.csv") -df.head()`, - mimeType: 'text/html', - cellType: 'code', - result: `A table`, - verifyValue: (d) => assert.ok(d.toString().includes(''), 'Table not found') - }, - { - // Plotly - 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/png', - cellType: 'code', - verifyValue: (d) => { return; } - } - ] - ); - - async function getNotebookCapableInterpreter(): Promise { - const is = ioc.serviceContainer.get(IInterpreterService); - const list = await is.getInterpreters(); - const procService = await processFactory.create(); - if (procService) { - for (let i = 0; i < list.length; i += 1) { - const result = await procService.exec(list[i].path, ['-m', 'jupyter', 'notebook', '--version'], { env: process.env }); - if (!result.stderr) { - return list[i]; - } - } - } - return undefined; - } - - async function generateNonDefaultConfig() { - const usable = await getNotebookCapableInterpreter(); - assert.ok(usable, 'Cant find jupyter enabled python'); - - // Manually generate an invalid jupyter config - const procService = await processFactory.create(); - assert.ok(procService, 'Can not get a process service'); - const results = await procService!.exec(usable!.path, ['-m', 'jupyter', 'notebook', '--generate-config', '-y'], { env: process.env }); - - // 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(IFileSystem); - await filesystem.writeFile(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 createNotebookServer(false); - 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 createNotebookServer(false); - } - }); - - runTest('Non default config does not mess up default config', async () => { - if (!ioc.mockJupyter) { - await generateNonDefaultConfig(); - const server = await createNotebookServer(true); - assert.ok(server, 'Never connected to a default server with a bad default config'); - - await verifySimple(server, `a=1${os.EOL}a`, 1); - } - }); - - runTest('Invalid kernel spec works', async () => { - if (ioc.mockJupyter) { - // Make a dummy class that will fail during launch - class FailedKernelSpec extends JupyterExecution { - protected async getMatchingKernelSpec(connection?: IConnection, cancelToken?: CancellationToken): Promise { - return Promise.resolve(undefined); - } - } - ioc.serviceManager.rebind(IJupyterExecution, FailedKernelSpec); - jupyterExecution = ioc.serviceManager.get(IJupyterExecution); - addMockData(`a=1${os.EOL}a`, 1); - - const server = await createNotebookServer(true); - assert.ok(server, 'Empty kernel spec messes up creating a server'); - - await verifySimple(server, `a=1${os.EOL}a`, 1); - } - }); - - // Tests that should be running: - // - Creation - // - Failure - // - Not installed - // - Different mime types - // - Export/import - // - Auto import - // - changing directories - // - Restart - // - Error types - // Test to write after jupyter process abstraction - // - jupyter not installed - // - kernel spec not matching - // - ipykernel not installed - // - kernelspec not installed - // - startup / shutdown / restart - make uses same kernelspec. Actually should be in memory already - // - Starting with python that doesn't have jupyter and make sure it can switch to one that does - // - Starting with python that doesn't have jupyter and make sure the switch still uses a python that's close as the kernel - -}); diff --git a/src/test/datascience/reactHelpers.ts b/src/test/datascience/reactHelpers.ts deleted file mode 100644 index e26eb163c62a..000000000000 --- a/src/test/datascience/reactHelpers.ts +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { ComponentClass, configure, ReactWrapper } from 'enzyme'; -import * as Adapter from 'enzyme-adapter-react-16'; -import { JSDOM } from 'jsdom'; -import * as React from 'react'; - -export function setUpDomEnvironment() { - // tslint:disable-next-line:no-http-string - const dom = new JSDOM('
', { pretendToBeVisual: true, url: 'http://localhost'}); - const { window } = dom; - - // tslint:disable-next-line:no-string-literal - global['window'] = window; - // tslint:disable-next-line:no-string-literal - global['document'] = window.document; - // tslint:disable-next-line:no-string-literal - global['navigator'] = { - userAgent: 'node.js', - platform: 'node' - }; - // tslint:disable-next-line:no-string-literal - global['self'] = window; - copyProps(window, global); - - // Special case. Transform needs createRange - // tslint:disable-next-line:no-string-literal - global['document'].createRange = () => ({ - createContextualFragment: str => JSDOM.fragment(str) - }); - - // For Jupyter server to load correctly. It expects the window object to not be defined - // tslint:disable-next-line:no-eval - const fetchMod = eval('require')('node-fetch'); - // tslint:disable-next-line:no-string-literal - global['fetch'] = fetchMod; - // tslint:disable-next-line:no-string-literal - global['Request'] = fetchMod.Request; - // tslint:disable-next-line:no-string-literal - global['Headers'] = fetchMod.Headers; - // tslint:disable-next-line:no-string-literal no-eval - global['WebSocket'] = eval('require')('ws'); - - // For the loc test to work, we have to have a global getter for loc strings - // tslint:disable-next-line:no-string-literal no-eval - global['getLocStrings'] = () => { - return { 'DataScience.unknownMimeType' : 'Unknown mime type from helper' }; - }; - - configure({ adapter: new Adapter() }); -} - -function copyProps(src, target) { - const props = Object.getOwnPropertyNames(src) - .filter(prop => typeof target[prop] === undefined); - props.forEach((p : string) => { - target[p] = src[p]; - }); -} - -function waitForComponentDidUpdate(component: React.Component) : Promise { - 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

, prevState: Readonly, 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(component: React.Component, numberOfRenders: number = 1) : Promise { - // 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'); - } - }); -} - -export async function waitForUpdate(wrapper: ReactWrapper, mainClass: ComponentClass

, numberOfRenders: number = 1) : Promise { - 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; - } -} diff --git a/src/test/debugger/attach.ptvsd.test.ts b/src/test/debugger/attach.ptvsd.test.ts deleted file mode 100644 index c9a90e3f659b..000000000000 --- a/src/test/debugger/attach.ptvsd.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../client/common/extensions'; - -import { ChildProcess, spawn } from 'child_process'; -import * as getFreePort from 'get-port'; -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import { DebugConfiguration, Uri } from 'vscode'; -import { DebugClient } from 'vscode-debugadapter-testsupport'; -import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import { IS_WINDOWS } from '../../client/common/platform/constants'; -import { FileSystem } from '../../client/common/platform/fileSystem'; -import { IPlatformService } from '../../client/common/platform/types'; -import { IConfigurationService } from '../../client/common/types'; -import { IMultiStepInputFactory } from '../../client/common/utils/multiStepInput'; -import { DebuggerTypeName, PTVSD_PATH } from '../../client/debugger/constants'; -import { PythonDebugConfigurationService } from '../../client/debugger/extension/configuration/debugConfigurationService'; -import { AttachConfigurationResolver } from '../../client/debugger/extension/configuration/resolvers/attach'; -import { IDebugConfigurationProviderFactory, IDebugConfigurationResolver } from '../../client/debugger/extension/configuration/types'; -import { AttachRequestArguments, DebugOptions, LaunchRequestArguments } from '../../client/debugger/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { PYTHON_PATH, sleep } from '../common'; -import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; -import { continueDebugging, createDebugAdapter } from './utils'; - -// tslint:disable:no-invalid-this max-func-body-length no-empty no-increment-decrement no-unused-variable no-console -const fileToDebug = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'remoteDebugger-start-with-ptvsd.py'); - -suite('Debugging - Attach Debugger', () => { - let debugClient: DebugClient; - let proc: ChildProcess; - - setup(async function () { - if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { - this.skip(); - } - this.timeout(30000); - const coverageDirectory = path.join(EXTENSION_ROOT_DIR, 'debug_coverage_attach_ptvsd'); - debugClient = await createDebugAdapter(coverageDirectory); - }); - teardown(async () => { - // Wait for a second before starting another test (sometimes, sockets take a while to get closed). - await sleep(1000); - try { - await debugClient.stop().catch(() => { }); - } catch (ex) { } - if (proc) { - try { - proc.kill(); - } catch { } - } - }); - async function testAttachingToRemoteProcess(localRoot: string, remoteRoot: string, isLocalHostWindows: boolean) { - const localHostPathSeparator = isLocalHostWindows ? '\\' : '/'; - const port = await getFreePort({ host: 'localhost', port: 3000 }); - const env = { ...process.env }; - - // Set the path for PTVSD to be picked up. - // tslint:disable-next-line:no-string-literal - env['PYTHONPATH'] = PTVSD_PATH; - const pythonArgs = ['-m', 'ptvsd', '--host', 'localhost', '--wait', '--port', `${port}`, '--file', fileToDebug.fileToCommandArgument()]; - proc = spawn(PYTHON_PATH, pythonArgs, { env: env, cwd: path.dirname(fileToDebug) }); - const exited = new Promise(resolve => proc.once('close', resolve)); - await sleep(3000); - - // Send initialize, attach - const initializePromise = debugClient.initializeRequest({ - adapterID: DebuggerTypeName, - linesStartAt1: true, - columnsStartAt1: true, - supportsRunInTerminalRequest: true, - pathFormat: 'path', - supportsVariableType: true, - supportsVariablePaging: true - }); - const options: AttachRequestArguments & DebugConfiguration = { - name: 'attach', - request: 'attach', - localRoot, - remoteRoot, - type: DebuggerTypeName, - port: port, - host: 'localhost', - logToFile: false, - debugOptions: [DebugOptions.RedirectOutput] - }; - const platformService = TypeMoq.Mock.ofType(); - platformService.setup(p => p.isWindows).returns(() => isLocalHostWindows); - const serviceContainer = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(IPlatformService, TypeMoq.It.isAny())).returns(() => platformService.object); - - const workspaceService = TypeMoq.Mock.ofType(); - const documentManager = TypeMoq.Mock.ofType(); - const configurationService = TypeMoq.Mock.ofType(); - - const launchResolver = TypeMoq.Mock.ofType>(); - const attachResolver = new AttachConfigurationResolver(workspaceService.object, documentManager.object, platformService.object, configurationService.object); - const providerFactory = TypeMoq.Mock.ofType().object; - const multiStepIput = TypeMoq.Mock.ofType().object; - const fs = mock(FileSystem); - const configProvider = new PythonDebugConfigurationService(attachResolver, launchResolver.object, providerFactory, multiStepIput, instance(fs)); - - await configProvider.resolveDebugConfiguration({ index: 0, name: 'root', uri: Uri.file(localRoot) }, options); - const attachPromise = debugClient.attachRequest(options); - - await Promise.all([ - initializePromise, - attachPromise, - debugClient.waitForEvent('initialized') - ]); - - const stdOutPromise = debugClient.assertOutput('stdout', 'this is stdout'); - const stdErrPromise = debugClient.assertOutput('stderr', 'this is stderr'); - - // Don't use path utils, as we're building the paths manually (mimic windows paths on unix test servers and vice versa). - const localFileName = `${localRoot}${localHostPathSeparator}${path.basename(fileToDebug)}`; - const breakpointLocation = { path: localFileName, column: 1, line: 12 }; - const breakpointPromise = debugClient.setBreakpointsRequest({ - lines: [breakpointLocation.line], - breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }], - source: { path: breakpointLocation.path } - }); - const exceptionBreakpointPromise = debugClient.setExceptionBreakpointsRequest({ filters: [] }); - const breakpointStoppedPromise = debugClient.assertStoppedLocation('breakpoint', breakpointLocation); - - await Promise.all([ - breakpointPromise, exceptionBreakpointPromise, - debugClient.configurationDoneRequest(), debugClient.threadsRequest(), - stdOutPromise, stdErrPromise, - breakpointStoppedPromise - ]); - - await continueDebugging(debugClient); - await exited; - } - test('Confirm we are able to attach to a running program', async () => { - await testAttachingToRemoteProcess(path.dirname(fileToDebug), path.dirname(fileToDebug), IS_WINDOWS); - }); -}); diff --git a/src/test/debugger/capabilities.test.ts b/src/test/debugger/capabilities.test.ts deleted file mode 100644 index eef8ecca7347..000000000000 --- a/src/test/debugger/capabilities.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-suspicious-comment max-func-body-length no-invalid-this no-var-requires no-require-imports no-any no-object-literal-type-assertion no-banned-terms - -import { expect } from 'chai'; -import { ChildProcess, spawn } from 'child_process'; -import * as getFreePort from 'get-port'; -import { Socket } from 'net'; -import * as path from 'path'; -import { Message } from 'vscode-debugadapter/lib/messages'; -import { DebugProtocol } from 'vscode-debugprotocol'; -import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import { createDeferred, sleep } from '../../client/common/utils/async'; -import { noop } from '../../client/common/utils/misc'; -import { PTVSD_PATH } from '../../client/debugger/constants'; -import { ProtocolParser } from '../../client/debugger/debugAdapter/Common/protocolParser'; -import { ProtocolMessageWriter } from '../../client/debugger/debugAdapter/Common/protocolWriter'; -import { PythonDebugger } from '../../client/debugger/debugAdapter/main'; -import { PYTHON_PATH } from '../common'; -import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; - -const fileToDebug = path.join(EXTENSION_ROOT_DIR, 'src', 'testMultiRootWkspc', 'workspace5', 'remoteDebugger-start-with-ptvsd-nowait.py'); - -suite('Debugging - Capabilities', function () { - this.timeout(30000); - let disposables: { dispose?: Function; destroy?: Function }[]; - let proc: ChildProcess; - setup(function () { - if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { - this.skip(); - } - disposables = []; - }); - teardown(() => { - disposables.forEach(disposable => { - try { - disposable.dispose!(); - } catch { - noop(); - } - try { - disposable.destroy!(); - } catch { - noop(); - } - }); - try { - proc.kill(); - } catch { - noop(); - } - }); - function createRequest(cmd: string, requestArgs: any) { - return new class extends Message implements DebugProtocol.InitializeRequest { - public arguments: any; - constructor(public command: string, args: any) { - super('request'); - this.arguments = args; - } - }(cmd, requestArgs); - } - function createDebugSession() { - return new class extends PythonDebugger { - constructor() { - super({} as any); - } - - public getInitializeResponseFromDebugAdapter() { - let initializeResponse = { - body: {} - } as DebugProtocol.InitializeResponse; - this.sendResponse = resp => initializeResponse = resp; - - this.initializeRequest(initializeResponse, { supportsRunInTerminalRequest: true, adapterID: '' }); - return initializeResponse; - } - }(); - } - test('Compare capabilities', async () => { - const customDebugger = createDebugSession(); - const expectedResponse = customDebugger.getInitializeResponseFromDebugAdapter(); - - const protocolWriter = new ProtocolMessageWriter(); - const initializeRequest: DebugProtocol.InitializeRequest = createRequest('initialize', { pathFormat: 'path' }); - const host = 'localhost'; - const port = await getFreePort({ host, port: 3000 }); - const env = { ...process.env }; - env.PYTHONPATH = PTVSD_PATH; - proc = spawn(PYTHON_PATH, ['-m', 'ptvsd', '--host', 'localhost', '--wait', '--port', `${port}`, '--file', fileToDebug], { cwd: path.dirname(fileToDebug), env }); - await sleep(3000); - - const connected = createDeferred(); - const socket = new Socket(); - socket.on('error', connected.reject.bind(connected)); - socket.connect({ port, host }, () => connected.resolve(socket)); - await connected.promise; - const protocolParser = new ProtocolParser(); - protocolParser.connect(socket!); - disposables.push(protocolParser); - const actualResponsePromise = new Promise(resolve => protocolParser.once('response_initialize', resolve)); - protocolWriter.write(socket, initializeRequest); - const actualResponse = await actualResponsePromise; - - const attachRequest: DebugProtocol.AttachRequest = createRequest('attach', { - name: 'attach', - request: 'attach', - type: 'python', - port: port, - host: 'localhost', - logToFile: false, - debugOptions: [] - }); - const attached = new Promise(resolve => protocolParser.once('response_attach', resolve)); - protocolWriter.write(socket, attachRequest); - await attached; - - const configRequest: DebugProtocol.ConfigurationDoneRequest = createRequest('configurationDone', {}); - const configured = new Promise(resolve => protocolParser.once('response_configurationDone', resolve)); - protocolWriter.write(socket, configRequest); - await configured; - - protocolParser.dispose(); - - // supportsDebuggerProperties is not documented, most probably a VS specific item. - const body: any = actualResponse.body; - delete body.supportsDebuggerProperties; - expect(actualResponse.body).to.deep.equal(expectedResponse.body); - }); -}); 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/debugStreamProvider.test.ts b/src/test/debugger/common/debugStreamProvider.test.ts deleted file mode 100644 index d25e6d95a17e..000000000000 --- a/src/test/debugger/common/debugStreamProvider.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import * as getFreePort from 'get-port'; -import * as net from 'net'; -import * as TypeMoq from 'typemoq'; -import { ICurrentProcess } from '../../../client/common/types'; -import { DebugStreamProvider } from '../../../client/debugger/debugAdapter/Common/debugStreamProvider'; -import { IDebugStreamProvider } from '../../../client/debugger/debugAdapter/types'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { sleep } from '../../common'; - -// tslint:disable-next-line:max-func-body-length -suite('Debugging - Stream Provider', () => { - let streamProvider: IDebugStreamProvider; - let serviceContainer: TypeMoq.IMock; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - streamProvider = new DebugStreamProvider(serviceContainer.object); - }); - test('Process is returned as is if there is no port number if args', async () => { - const mockProcess = { argv: [], env: [], stdin: '1234', stdout: '5678' }; - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => mockProcess); - - const streams = await streamProvider.getInputAndOutputStreams(); - expect(streams.input).to.be.equal(mockProcess.stdin); - expect(streams.output).to.be.equal(mockProcess.stdout); - }); - test('Starts a socketserver on the port provided and returns the client socket', async () => { - const port = await getFreePort({ host: 'localhost', port: 3000 }); - const mockProcess = { argv: ['node', 'index.js', `--server=${port}`], env: [], stdin: '1234', stdout: '5678' }; - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => mockProcess); - - const streamsPromise = streamProvider.getInputAndOutputStreams(); - await sleep(1); - - await new Promise(resolve => { - net.connect({ port, host: 'localhost' }, resolve); - }); - - const streams = await streamsPromise; - expect(streams.input).to.not.be.equal(mockProcess.stdin); - expect(streams.output).to.not.be.equal(mockProcess.stdout); - }); - test('Ensure existence of port is identified', async () => { - const port = await getFreePort({ host: 'localhost', port: 3000 }); - const mockProcess = { argv: ['node', 'index.js', `--server=${port}`], env: [], stdin: '1234', stdout: '5678' }; - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => mockProcess); - - expect(streamProvider.useDebugSocketStream).to.be.equal(true, 'incorrect'); - }); - test('Ensure non-existence of port is identified', async () => { - const port = await getFreePort({ host: 'localhost', port: 3000 }); - const mockProcess = { argv: ['node', 'index.js', `--other=${port}`], env: [], stdin: '1234', stdout: '5678' }; - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => mockProcess); - - expect(streamProvider.useDebugSocketStream).to.not.be.equal(true, 'incorrect'); - }); -}); diff --git a/src/test/debugger/common/protocolWriter.test.ts b/src/test/debugger/common/protocolWriter.test.ts deleted file mode 100644 index b55d438f4f70..000000000000 --- a/src/test/debugger/common/protocolWriter.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// tslint:disable:no-any - -import { expect } from 'chai'; -import { Transform } from 'stream'; -import { InitializedEvent } from 'vscode-debugadapter/lib/main'; -import { ProtocolMessageWriter } from '../../../client/debugger/debugAdapter/Common/protocolWriter'; - -suite('Debugging - Protocol Writer', () => { - test('Test request, response and event messages', async () => { - let dataWritten = ''; - const throughOutStream = new Transform({ - transform: (chunk, encoding, callback) => { - dataWritten += (chunk as Buffer).toString('utf8'); - callback(null, chunk); - } - }); - - const message = new InitializedEvent(); - message.seq = 123; - const writer = new ProtocolMessageWriter(); - writer.write(throughOutStream, message); - - const json = JSON.stringify(message); - const expectedMessage = `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`; - expect(dataWritten).to.be.equal(expectedMessage); - }); -}); diff --git a/src/test/debugger/common/protocoloLogger.test.ts b/src/test/debugger/common/protocoloLogger.test.ts deleted file mode 100644 index c82cfbe84e22..000000000000 --- a/src/test/debugger/common/protocoloLogger.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { PassThrough } from 'stream'; -import * as TypeMoq from 'typemoq'; -import { Logger } from 'vscode-debugadapter'; -import { ProtocolLogger } from '../../../client/debugger/debugAdapter/Common/protocolLogger'; -import { IProtocolLogger } from '../../../client/debugger/debugAdapter/types'; - -// tslint:disable-next-line:max-func-body-length -suite('Debugging - Protocol Logger', () => { - let protocolLogger: IProtocolLogger; - setup(() => { - protocolLogger = new ProtocolLogger(); - }); - test('Ensure messages are buffered untill logger is provided', async () => { - const inputStream = new PassThrough(); - const outputStream = new PassThrough(); - - protocolLogger.connect(inputStream, outputStream); - - inputStream.write('A'); - outputStream.write('1'); - - inputStream.write('B'); - inputStream.write('C'); - - outputStream.write('2'); - outputStream.write('3'); - - const logger = TypeMoq.Mock.ofType(); - protocolLogger.setup(logger.object); - - logger.verify(l => l.verbose('From Client:'), TypeMoq.Times.exactly(3)); - logger.verify(l => l.verbose('To Client:'), TypeMoq.Times.exactly(3)); - - const expectedLogFileContents = ['A', '1', 'B', 'C', '2', '3']; - for (const expectedContent of expectedLogFileContents) { - logger.verify(l => l.verbose(expectedContent), TypeMoq.Times.once()); - } - }); - test('Ensure messages are are logged as they arrive', async () => { - const inputStream = new PassThrough(); - const outputStream = new PassThrough(); - - protocolLogger.connect(inputStream, outputStream); - - inputStream.write('A'); - outputStream.write('1'); - - const logger = TypeMoq.Mock.ofType(); - protocolLogger.setup(logger.object); - - inputStream.write('B'); - inputStream.write('C'); - - outputStream.write('2'); - outputStream.write('3'); - - logger.verify(l => l.verbose('From Client:'), TypeMoq.Times.exactly(3)); - logger.verify(l => l.verbose('To Client:'), TypeMoq.Times.exactly(3)); - - const expectedLogFileContents = ['A', '1', 'B', 'C', '2', '3']; - for (const expectedContent of expectedLogFileContents) { - logger.verify(l => l.verbose(expectedContent), TypeMoq.Times.once()); - } - }); - test('Ensure nothing is logged once logging is disabled', async () => { - const inputStream = new PassThrough(); - const outputStream = new PassThrough(); - - protocolLogger.connect(inputStream, outputStream); - const logger = TypeMoq.Mock.ofType(); - protocolLogger.setup(logger.object); - - inputStream.write('A'); - outputStream.write('1'); - - protocolLogger.dispose(); - - inputStream.write('B'); - inputStream.write('C'); - - outputStream.write('2'); - outputStream.write('3'); - - logger.verify(l => l.verbose('From Client:'), TypeMoq.Times.exactly(1)); - logger.verify(l => l.verbose('To Client:'), TypeMoq.Times.exactly(1)); - - const expectedLogFileContents = ['A', '1']; - const notExpectedLogFileContents = ['B', 'C', '2', '3']; - - for (const expectedContent of expectedLogFileContents) { - logger.verify(l => l.verbose(expectedContent), TypeMoq.Times.once()); - } - for (const notExpectedContent of notExpectedLogFileContents) { - logger.verify(l => l.verbose(notExpectedContent), TypeMoq.Times.never()); - } - }); -}); diff --git a/src/test/debugger/common/protocolparser.test.ts b/src/test/debugger/common/protocolparser.test.ts deleted file mode 100644 index b9d02de93b8d..000000000000 --- a/src/test/debugger/common/protocolparser.test.ts +++ /dev/null @@ -1,62 +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/debugAdapter/Common/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(resolve => { - protocolParser.on('request_initialize', () => resolve(true)); - }); - const responseDetected = new Promise(resolve => { - protocolParser.on('response_initialize', () => resolve(true)); - }); - const eventDetected = new Promise(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(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(); - 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/debugAdapter/debugClients/launcherProvider.unit.test.ts b/src/test/debugger/debugAdapter/debugClients/launcherProvider.unit.test.ts deleted file mode 100644 index 4dd9542a6b0c..000000000000 --- a/src/test/debugger/debugAdapter/debugClients/launcherProvider.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 * as fs from 'fs-extra'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../../../client/common/constants'; -import { DebuggerLauncherScriptProvider, NoDebugLauncherScriptProvider, RemoteDebuggerLauncherScriptProvider } from '../../../../client/debugger/debugAdapter/DebugClients/launcherProvider'; - -const expectedPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'ptvsd_launcher.py'); - -suite('Debugger - Launcher Script Provider', () => { - test('Ensure launcher script exists', async () => { - expect(await fs.pathExists(expectedPath)).to.be.deep.equal(true, 'Debugger launcher script does not exist'); - }); - test('Test debug launcher args', async () => { - const args = new DebuggerLauncherScriptProvider().getLauncherArgs({ host: 'something', port: 1234 }); - const expectedArgs = [expectedPath, '--default', '--client', '--host', 'something', '--port', '1234']; - expect(args).to.be.deep.equal(expectedArgs); - }); - test('Test non-debug launcher args', async () => { - const args = new NoDebugLauncherScriptProvider().getLauncherArgs({ host: 'something', port: 1234 }); - const expectedArgs = [expectedPath, '--default', '--nodebug', '--client', '--host', 'something', '--port', '1234']; - expect(args).to.be.deep.equal(expectedArgs); - }); - test('Test debug launcher args and custom ptvsd', async () => { - const args = new DebuggerLauncherScriptProvider().getLauncherArgs({ host: 'something', port: 1234, customDebugger: true }); - const expectedArgs = [expectedPath, '--custom', '--client', '--host', 'something', '--port', '1234']; - expect(args).to.be.deep.equal(expectedArgs); - }); - test('Test non-debug launcher args and custom ptvsd', async () => { - const args = new NoDebugLauncherScriptProvider().getLauncherArgs({ host: 'something', port: 1234, customDebugger: true }); - const expectedArgs = [expectedPath, '--custom', '--nodebug', '--client', '--host', 'something', '--port', '1234']; - expect(args).to.be.deep.equal(expectedArgs); - }); - test('Test remote debug launcher args (and do not wait for debugger to attach)', async () => { - const args = new RemoteDebuggerLauncherScriptProvider().getLauncherArgs({ host: 'something', port: 1234, waitUntilDebuggerAttaches: false }); - const expectedArgs = [expectedPath, '--default', '--host', 'something', '--port', '1234']; - expect(args).to.be.deep.equal(expectedArgs); - }); - test('Test remote debug launcher args (and wait for debugger to attach)', async () => { - const args = new RemoteDebuggerLauncherScriptProvider().getLauncherArgs({ host: 'something', port: 1234, waitUntilDebuggerAttaches: true }); - const expectedArgs = [expectedPath, '--default', '--host', 'something', '--port', '1234', '--wait']; - expect(args).to.be.deep.equal(expectedArgs); - }); -}); diff --git a/src/test/debugger/debugClient.ts b/src/test/debugger/debugClient.ts deleted file mode 100644 index 318d6255b695..000000000000 --- a/src/test/debugger/debugClient.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { ChildProcess, spawn, SpawnOptions } from 'child_process'; -import * as path from 'path'; -import { DebugClient } from 'vscode-debugadapter-testsupport'; -import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import { noop } from '../../client/common/utils/misc'; - -export class DebugClientEx extends DebugClient { - private adapterProcess: ChildProcess | undefined; - constructor(private executable: string, debugType: string, private coverageDirectory: string, private spawnOptions?: SpawnOptions) { - super('node', '', debugType, spawnOptions); - } - /** - * Starts a new debug adapter and sets up communication via stdin/stdout. - * If a port number is specified the adapter is not launched but a connection to - * a debug adapter running in server mode is established. This is useful for debugging - * the adapter while running tests. For this reason all timeouts are disabled in server mode. - */ - public start(port?: number): Promise { - return new Promise((resolve, reject) => { - const runtime = path.join(EXTENSION_ROOT_DIR, 'node_modules', '.bin', 'istanbul'); - const args = ['cover', '--report=json', '--print=none', `--dir=${this.coverageDirectory}`, '--handle-sigint', this.executable]; - this.adapterProcess = spawn(runtime, args, this.spawnOptions); - this.adapterProcess.stderr.on('data', noop); - this.adapterProcess.on('error', (err) => { - console.error(err); - reject(err); - }); - this.adapterProcess.on('exit', noop); - this.connect(this.adapterProcess.stdout, this.adapterProcess.stdin); - resolve(); - }); - } - public stop(): Promise { - return this.disconnectRequest().then(this.stopAdapterProcess).catch(this.stopAdapterProcess); - } - private stopAdapterProcess = () => { - if (this.adapterProcess) { - this.adapterProcess.kill(); - this.adapterProcess = undefined; - } - } -} diff --git a/src/test/debugger/envVars.test.ts b/src/test/debugger/envVars.test.ts index 4b86d6c12930..8b0f55986281 100644 --- a/src/test/debugger/envVars.test.ts +++ b/src/test/debugger/envVars.test.ts @@ -1,34 +1,46 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// tslint:disable:no-string-literal no-unused-expression chai-vague-errors max-func-body-length no-any - import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; import * as shortid from 'shortid'; import { ICurrentProcess, IPathUtils } from '../../client/common/types'; import { IEnvironmentVariablesService } from '../../client/common/variables/types'; -import { DebugClientHelper } from '../../client/debugger/debugAdapter/DebugClients/helper'; -import { LaunchRequestArguments } from '../../client/debugger/types'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; -import { UnitTestIocContainer } from '../unittests/serviceRegistry'; +import { + DebugEnvironmentVariablesHelper, + IDebugEnvironmentVariablesService, +} from '../../client/debugger/extension/configuration/resolvers/helper'; +import { ConsoleType, LaunchRequestArguments } from '../../client/debugger/types'; +import { isOs, OSType } from '../common'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; +import { UnitTestIocContainer } from '../testing/serviceRegistry'; +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; - let helper: DebugClientHelper; + let debugEnvParser: IDebugEnvironmentVariablesService; let pathVariableName: string; let mockProcess: ICurrentProcess; - suiteSetup(initialize); + + suiteSetup(async function () { + if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + return this.skip(); + } + await initialize(); + }); + setup(async () => { - initializeDI(); + await initializeDI(); await initializeTest(); const envParser = ioc.serviceContainer.get(IEnvironmentVariablesService); const pathUtils = ioc.serviceContainer.get(IPathUtils); mockProcess = ioc.serviceContainer.get(ICurrentProcess); - helper = new DebugClientHelper(envParser, pathUtils, mockProcess); + debugEnvParser = new DebugEnvironmentVariablesHelper(envParser, mockProcess); pathVariableName = pathUtils.getPathVariableName(); }); suiteTeardown(closeActiveWindows); @@ -37,30 +49,60 @@ 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, + RecommendedEnvironmentService, + ); } - async function testBasicProperties(console: 'externalTerminal' | 'integratedTerminal' | 'none', expectedNumberOfVariables: number) { - const args = { - program: '', pythonPath: '', args: [], envFile: '', - console - // tslint:disable-next-line:no-any - } as any as LaunchRequestArguments; + async function testBasicProperties(console: ConsoleType, expectedNumberOfVariables: number) { + const args = ({ + program: '', + pythonPath: '', + args: [], + envFile: '', + console, + } as any) as LaunchRequestArguments; - const envVars = await helper.getEnvironmentVariables(args); + const envVars = await debugEnvParser.getEnvironmentVariables(args); expect(envVars).not.be.undefined; expect(Object.keys(envVars)).lengthOf(expectedNumberOfVariables, 'Incorrect number of variables'); expect(envVars).to.have.property('PYTHONUNBUFFERED', '1', 'Property not found'); expect(envVars).to.have.property('PYTHONIOENCODING', 'UTF-8', 'Property not found'); } - test('Confirm basic environment variables exist when launched in external terminal', () => testBasicProperties('externalTerminal', 2)); + test('Confirm basic environment variables exist when launched in external terminal', () => + testBasicProperties('externalTerminal', 2)); - test('Confirm basic environment variables exist when launched in intergrated terminal', () => testBasicProperties('integratedTerminal', 2)); + test('Confirm basic environment variables exist when launched in intergrated terminal', () => + testBasicProperties('integratedTerminal', 2)); + + test('Confirm base environment variables are merged without overwriting when provided', async () => { + const env: Record = { 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; @@ -70,27 +112,29 @@ suite('Resolving Environment Variables when Debugging', () => { if (mockProcess.env['PYTHONIOENCODING'] === undefined) { expectedNumberOfVariables += 1; } - await testBasicProperties('none', expectedNumberOfVariables); + await testBasicProperties('internalConsole', expectedNumberOfVariables); }); - async function testJsonEnvVariables(console: 'externalTerminal' | 'integratedTerminal' | 'none', expectedNumberOfVariables: number) { - const prop1 = shortid.generate(); - const prop2 = shortid.generate(); - const prop3 = shortid.generate(); - const env = {}; + async function testJsonEnvVariables(console: ConsoleType, expectedNumberOfVariables: number) { + const prop1 = normCase(shortid.generate()); + const prop2 = normCase(shortid.generate()); + const prop3 = normCase(shortid.generate()); + const env: Record = {}; env[prop1] = prop1; env[prop2] = prop2; mockProcess.env[prop3] = prop3; - const args = { - program: '', pythonPath: '', args: [], envFile: '', - console, env - // tslint:disable-next-line:no-any - } as any as LaunchRequestArguments; + const args = ({ + program: '', + pythonPath: '', + args: [], + envFile: '', + console, + env, + } as any) as LaunchRequestArguments; - const envVars = await helper.getEnvironmentVariables(args); + 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'); @@ -98,16 +142,18 @@ suite('Resolving Environment Variables when Debugging', () => { expect(envVars).to.have.property(prop1, prop1, 'Property not found'); expect(envVars).to.have.property(prop2, prop2, 'Property not found'); - if (console === 'none') { + if (console === 'internalConsole') { expect(envVars).to.have.property(prop3, prop3, 'Property not found'); } else { expect(envVars).not.to.have.property(prop3, prop3, 'Property not found'); } } - test('Confirm json environment variables exist when launched in external terminal', () => testJsonEnvVariables('externalTerminal', 2 + 2)); + test('Confirm json environment variables exist when launched in external terminal', () => + testJsonEnvVariables('externalTerminal', 2 + 2)); - test('Confirm json environment variables exist when launched in intergrated terminal', () => testJsonEnvVariables('integratedTerminal', 2 + 2)); + test('Confirm json environment variables exist when launched in intergrated terminal', () => + testJsonEnvVariables('integratedTerminal', 2 + 2)); test('Confirm json environment variables exist when launched in debug console', async () => { // Add 3 for the 3 new json env variables @@ -118,11 +164,14 @@ suite('Resolving Environment Variables when Debugging', () => { if (mockProcess.env['PYTHONIOENCODING'] === undefined) { expectedNumberOfVariables += 1; } - await testJsonEnvVariables('none', expectedNumberOfVariables); + await testJsonEnvVariables('internalConsole', expectedNumberOfVariables); }); - async function testAppendingOfPaths(console: 'externalTerminal' | 'integratedTerminal' | 'none', - expectedNumberOfVariables: number, removePythonPath: boolean) { + async function testAppendingOfPaths( + console: ConsoleType, + expectedNumberOfVariables: number, + removePythonPath: boolean, + ) { if (removePythonPath && mockProcess.env.PYTHONPATH !== undefined) { delete mockProcess.env.PYTHONPATH; } @@ -133,19 +182,23 @@ suite('Resolving Environment Variables when Debugging', () => { const prop2 = shortid.generate(); const prop3 = shortid.generate(); - const env = {}; + const env: Record = {}; env[pathVariableName] = customPathToAppend; env['PYTHONPATH'] = customPythonPathToAppend; env[prop1] = prop1; env[prop2] = prop2; mockProcess.env[prop3] = prop3; - const args = { - program: '', pythonPath: '', args: [], envFile: '', - console, env - } as any as LaunchRequestArguments; + const args = ({ + program: '', + pythonPath: '', + args: [], + envFile: '', + console, + env, + } as any) as LaunchRequestArguments; - const envVars = await helper.getEnvironmentVariables(args); + const envVars = await debugEnvParser.getEnvironmentVariables(args); expect(envVars).not.be.undefined; expect(Object.keys(envVars)).lengthOf(expectedNumberOfVariables, 'Incorrect number of variables'); expect(envVars).to.have.property('PYTHONPATH'); @@ -155,14 +208,14 @@ suite('Resolving Environment Variables when Debugging', () => { expect(envVars).to.have.property(prop1, prop1, 'Property not found'); expect(envVars).to.have.property(prop2, prop2, 'Property not found'); - if (console === 'none') { + if (console === 'internalConsole') { expect(envVars).to.have.property(prop3, prop3, 'Property not found'); } else { expect(envVars).not.to.have.property(prop3, prop3, 'Property not found'); } // Confirm the paths have been appended correctly. - const expectedPath = customPathToAppend + path.delimiter + mockProcess.env[pathVariableName]; + const expectedPath = `${customPathToAppend}${path.delimiter}${mockProcess.env[pathVariableName]}`; expect(envVars).to.have.property(pathVariableName, expectedPath, 'PATH is not correct'); // Confirm the paths have been appended correctly. @@ -172,23 +225,46 @@ suite('Resolving Environment Variables when Debugging', () => { } expect(envVars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH is not correct'); - if (console === 'none') { + if (console === 'internalConsole') { // All variables in current process must be in here - expect(Object.keys(envVars).length).greaterThan(Object.keys(mockProcess.env).length, 'Variables is not a subset'); - Object.keys(mockProcess.env).forEach(key => { + expect(Object.keys(envVars).length).greaterThan( + Object.keys(mockProcess.env).length, + 'Variables is not a subset', + ); + Object.keys(mockProcess.env).forEach((key) => { if (key === pathVariableName || key === 'PYTHONPATH') { return; } - expect(mockProcess.env[key]).equal(envVars[key], `Value for the environment variable '${key}' is incorrect.`); + expect(mockProcess.env[key]).equal( + envVars[key], + `Value for the environment variable '${key}' is incorrect.`, + ); }); } } - test('Confirm paths get appended correctly when using json variables and launched in external terminal', () => testAppendingOfPaths('externalTerminal', 6, false)); + 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)) { + return this.skip(); + } + await testAppendingOfPaths('externalTerminal', 6, false); + }); + + test('Confirm paths get appended correctly when using json variables and launched in integrated terminal', async function () { + // test is flakey on windows, path separator problems. GH issue #4758 + if (isOs(OSType.Windows)) { + return this.skip(); + } + await testAppendingOfPaths('integratedTerminal', 6, false); + }); - test('Confirm paths get appended correctly when using json variables and launched in integrated terminal', () => testAppendingOfPaths('integratedTerminal', 6, false)); + test('Confirm paths get appended correctly when using json variables and launched in debug console', async function () { + // test is flakey on windows, path separator problems. GH issue #4758 + if (isOs(OSType.Windows)) { + return this.skip(); + } - test('Confirm paths get appended correctly when using json variables and launched in debug console', async () => { // Add 3 for the 3 new json env variables let expectedNumberOfVariables = Object.keys(mockProcess.env).length + 3; if (mockProcess.env['PYTHONUNBUFFERED'] === undefined) { @@ -200,6 +276,6 @@ suite('Resolving Environment Variables when Debugging', () => { if (mockProcess.env['PYTHONIOENCODING'] === undefined) { expectedNumberOfVariables += 1; } - await testAppendingOfPaths('none', expectedNumberOfVariables, false); + await testAppendingOfPaths('internalConsole', expectedNumberOfVariables, false); }); }); diff --git a/src/test/debugger/extension/adapter/adapter.test.ts b/src/test/debugger/extension/adapter/adapter.test.ts new file mode 100644 index 000000000000..cd53b41102ab --- /dev/null +++ b/src/test/debugger/extension/adapter/adapter.test.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as fs from '../../../../client/common/platform/fs-paths'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { openFile } from '../../../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../../../initialize'; +import { DebuggerFixture } from '../../utils'; + +const WS_ROOT = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test'); + +function resolveWSFile(wsRoot: string, ...filePath: string[]): string { + return path.join(wsRoot, ...filePath); +} + +suite('Debugger Integration', () => { + const file = resolveWSFile(WS_ROOT, '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); + const defaultScriptArgs = [doneFile]; + let workspaceRoot: vscode.WorkspaceFolder; + let fix: DebuggerFixture; + suiteSetup(async function () { + if (IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { + this.skip(); + } + await initialize(); + const ws = vscode.workspace.getWorkspaceFolder(resource); + workspaceRoot = ws!; + expect(workspaceRoot).to.not.equal(undefined, 'missing workspace root'); + }); + setup(async () => { + fix = new DebuggerFixture(); + await initializeTest(); + await openFile(file); + }); + teardown(async () => { + await fix.cleanUp(); + fix.addFSCleanup(outFile); + await closeActiveWindows(); + }); + async function setDone() { + await fs.writeFile(doneFile, ''); + fix.addFSCleanup(doneFile); + } + + type ConfigName = string; + type ScriptArgs = string[]; + const tests: { [key: string]: [ConfigName, ScriptArgs] } = { + // prettier-ignore + 'launch': ['launch a file', [...defaultScriptArgs, outFile]], + // prettier-ignore + 'attach': ['attach to a local port', defaultScriptArgs], + 'attach to PID': ['attach to a local PID', defaultScriptArgs], + // For now we do not worry about "test" debugging. + }; + + suite('run to end', () => { + for (const kind of Object.keys(tests)) { + if (kind === 'attach to PID') { + // Attach-to-pid is still a little finicky + // so we're skipping it for now. + continue; + } + const [configName, scriptArgs] = tests[kind]; + test(kind, async () => { + const session = 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 + await setDone(); + const result = await session.waitUntilDone(); + + expect(result.exitCode).to.equal(0, 'bad exit code'); + const output = result.stdout !== '' ? result.stdout : fs.readFileSync(outFile).toString(); + expect(output.trim().endsWith('done!')).to.equal(true, `bad output\n${output}`); + }); + } + }); + + suite('handles breakpoint', () => { + for (const kind of ['launch', 'attach']) { + if (kind === 'attach') { + // The test isn't working quite right for attach + // so we skip it for now. + continue; + } + const [configName, scriptArgs] = tests[kind]; + test(kind, async () => { + const session = await fix.resolveDebugger(configName, file, scriptArgs, workspaceRoot); + const bp = session.addBreakpoint(file, 21); // line: "time.sleep()" + await session.start(); + await session.waitForBreakpoint(bp); + await setDone(); + const result = await session.waitUntilDone(); + + expect(result.exitCode).to.equal(0, 'bad exit code'); + const output = result.stdout !== '' ? result.stdout : fs.readFileSync(outFile).toString(); + expect(output.trim().endsWith('done!')).to.equal(true, `bad output\n${output}`); + }); + } + }); +}); diff --git a/src/test/debugger/extension/adapter/factory.unit.test.ts b/src/test/debugger/extension/adapter/factory.unit.test.ts new file mode 100644 index 000000000000..50984327e40d --- /dev/null +++ b/src/test/debugger/extension/adapter/factory.unit.test.ts @@ -0,0 +1,315 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as fs from '../../../../client/common/platform/fs-paths'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import rewiremock from 'rewiremock'; +import { SemVer } from 'semver'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { DebugAdapterExecutable, DebugAdapterServer, DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; +import { ConfigurationService } from '../../../../client/common/configuration/service'; +import { IPersistentStateFactory, IPythonSettings } from '../../../../client/common/types'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +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 { EnvironmentType } from '../../../../client/pythonEnvironments/info'; +import { clearTelemetryReporter } from '../../../../client/telemetry'; +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.default); + +suite('Debugging - Adapter Factory', () => { + let factory: IDebugAdapterDescriptorFactory; + let interpreterService: IInterpreterService; + let stateFactory: IPersistentStateFactory; + let state: PersistentState; + let showErrorMessageStub: sinon.SinonStub; + let readJSONSyncStub: sinon.SinonStub; + let commandManager: ICommandManager; + let getDebugpyPathStub: sinon.SinonStub; + + const nodeExecutable = undefined; + 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: '', + envType: EnvironmentType.Unknown, + version: new SemVer('3.7.4-test'), + }; + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + + class Reporter { + public static eventNames: string[] = []; + public static properties: Record[] = []; + public static measures: {}[] = []; + public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { + Reporter.eventNames.push(eventName); + Reporter.properties.push(properties!); + Reporter.measures.push(measures!); + } + } + + setup(() => { + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + readJSONSyncStub = sinon.stub(fs, 'readJSONSync'); + readJSONSyncStub.returns({ enableTelemetry: true }); + rewiremock.enable(); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState) as PersistentState; + commandManager = mock(CommandManager); + getDebugpyPathStub = sinon.stub(pythonDebugger, 'getDebugpyPath'); + getDebugpyPathStub.resolves(debugpyPath); + showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); + + when( + stateFactory.createGlobalPersistentState(debugStateKeys.doNotShowAgain, false), + ).thenReturn(instance(state)); + + const configurationService = mock(ConfigurationService); + when(configurationService.getSettings(undefined)).thenReturn(({ + experiments: { enabled: true }, + } as any) as IPythonSettings); + + interpreterService = mock(InterpreterService); + + when(interpreterService.getInterpreterDetails(pythonPath)).thenResolve(interpreter); + when(interpreterService.getInterpreters(anything())).thenReturn([interpreter]); + + factory = new DebugAdapterDescriptorFactory( + instance(commandManager), + instance(interpreterService), + instance(stateFactory), + ); + }); + + 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(); + sinon.restore(); + }); + + function createSession(config: Partial, workspaceFolder?: WorkspaceFolder): DebugSession { + return { + configuration: { name: '', request: 'launch', type: 'python', ...config }, + id: '', + name: 'python', + type: 'python', + workspaceFolder, + customRequest: () => Promise.resolve(), + getDebugProtocolBreakpoint: () => Promise.resolve(undefined), + }; + } + + test('Return the value of configuration.pythonPath as the current python path if it exists', async () => { + const session = createSession({ pythonPath }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.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 () => { + const session = createSession({}); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + 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 () => { + const session = createSession({}); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Display a message if no python interpreter is set', async () => { + 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'); + 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 () => { + const session = createSession({ request: 'attach', port: 5678, host: 'localhost' }); + const debugServer = new DebugAdapterServer(session.configuration.port, session.configuration.host); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + // Interpreter not needed for host/port + verify(interpreterService.getInterpreters(anything())).never(); + assert.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, + ); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + // Interpreter not needed for connect + verify(interpreterService.getInterpreters(anything())).never(); + assert.deepStrictEqual(descriptor, debugServer); + }); + + test('Return Debug Adapter executable if request is "attach", and listen is specified', async () => { + const session = createSession({ request: 'attach', listen: { port: 5678, host: 'localhost' } }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test('Throw error if request is "attach", and neither port, processId, listen, nor connect is specified', async () => { + const session = createSession({ + request: 'attach', + port: undefined, + processId: undefined, + listen: undefined, + connect: undefined, + }); + + const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable); + + await expect(promise).to.eventually.be.rejectedWith( + '"request":"attach" requires either "connect", "listen", or "processId"', + ); + }); + + test('Pass the --log-dir argument to debug adapter if configuration.logToFile is set', async () => { + const session = createSession({ logToFile: true }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [ + debugAdapterPath, + '--log-dir', + EXTENSION_ROOT_DIR, + ]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test("Don't pass the --log-dir argument to debug adapter if configuration.logToFile is not set", async () => { + const session = createSession({}); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.deepStrictEqual(descriptor, debugExecutable); + }); + + test("Don't pass the --log-dir argument to debugger if configuration.logToFile is set to false", async () => { + const session = createSession({ logToFile: false }); + const debugExecutable = new DebugAdapterExecutable(pythonPath, [debugAdapterPath]); + + const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + assert.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); + }); + + test("Don't send any telemetry if not attaching to a local process", async () => { + const session = createSession({}); + + await factory.createDebugAdapterDescriptor(session, nodeExecutable); + }); + + 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.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 new file mode 100644 index 000000000000..18fbb2b66058 --- /dev/null +++ b/src/test/debugger/extension/adapter/logging.unit.test.ts @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { DebugSession, WorkspaceFolder } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +import { FileSystem } from '../../../../client/common/platform/fileSystem'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { DebugSessionLoggingFactory } from '../../../../client/debugger/extension/adapter/logging'; + +suite('Debugging - Session Logging', () => { + const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; + const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + let loggerFactory: DebugSessionLoggingFactory; + let fsService: FileSystem; + let writeStream: fs.WriteStream; + + setup(() => { + fsService = mock(FileSystem); + writeStream = mock(fs.WriteStream); + + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + + loggerFactory = new DebugSessionLoggingFactory(instance(fsService)); + }); + + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + }); + + function createSession(id: string, workspaceFolder?: WorkspaceFolder): DebugSession { + return { + configuration: { + name: '', + request: 'launch', + type: 'python', + }, + id: id, + name: 'python', + type: 'python', + workspaceFolder, + customRequest: () => Promise.resolve(), + getDebugProtocolBreakpoint: () => Promise.resolve(undefined), + }; + } + + function createSessionWithLogging(id: string, logToFile: boolean, workspaceFolder?: WorkspaceFolder): DebugSession { + const session = createSession(id, workspaceFolder); + session.configuration.logToFile = logToFile; + return session; + } + + class TestMessage implements DebugProtocol.ProtocolMessage { + public seq: number; + public type: string; + public id: number; + public format: string; + public variables?: { [key: string]: string }; + public sendTelemetry?: boolean; + public showUser?: boolean; + public url?: string; + public urlLabel?: string; + constructor(id: number, seq: number, type: string) { + this.id = id; + this.format = 'json'; + this.seq = seq; + this.type = type; + } + } + + test('Create logger using session without logToFile', async () => { + const session = createSession('test1'); + const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); + + await loggerFactory.createDebugAdapterTracker(session); + + verify(fsService.createWriteStream(filePath)).never(); + }); + + test('Create logger using session with logToFile set to false', async () => { + const session = createSessionWithLogging('test2', false); + const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); + + when(fsService.createWriteStream(filePath)).thenReturn(instance(writeStream)); + when(writeStream.write(anything())).thenReturn(true); + const logger = await loggerFactory.createDebugAdapterTracker(session); + if (logger) { + logger.onWillStartSession!(); + } + + verify(fsService.createWriteStream(filePath)).never(); + verify(writeStream.write(anything())).never(); + }); + + test('Create logger using session with logToFile set to true', async () => { + const session = createSessionWithLogging('test3', true); + const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`); + const logs: string[] = []; + + when(fsService.createWriteStream(filePath)).thenReturn(instance(writeStream)); + when(writeStream.write(anything())).thenCall((msg) => logs.push(msg)); + + const message = new TestMessage(1, 1, 'test-message'); + const logger = await loggerFactory.createDebugAdapterTracker(session); + + if (logger) { + logger.onWillStartSession!(); + assert.ok(logs.pop()!.includes('Starting Session')); + + logger.onDidSendMessage!(message); + const sentLog = logs.pop(); + assert.ok(sentLog!.includes('Client <-- Adapter')); + assert.ok(sentLog!.includes('test-message')); + + logger.onWillReceiveMessage!(message); + const receivedLog = logs.pop(); + assert.ok(receivedLog!.includes('Client --> Adapter')); + assert.ok(receivedLog!.includes('test-message')); + + logger.onWillStopSession!(); + assert.ok(logs.pop()!.includes('Stopping Session')); + + logger.onError!(new Error('test error message')); + assert.ok(logs.pop()!.includes('Error')); + + logger.onExit!(111, '222'); + const exitLog1 = logs.pop(); + assert.ok(exitLog1!.includes('Exit-Code: 111')); + assert.ok(exitLog1!.includes('Signal: 222')); + + logger.onExit!(undefined, undefined); + const exitLog2 = logs.pop(); + assert.ok(exitLog2!.includes('Exit-Code: 0')); + assert.ok(exitLog2!.includes('Signal: none')); + } + + verify(fsService.createWriteStream(filePath)).once(); + verify(writeStream.write(anything())).times(7); + assert.deepEqual(logs, []); + }); +}); diff --git a/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts b/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts new file mode 100644 index 000000000000..9f9497317417 --- /dev/null +++ b/src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts @@ -0,0 +1,180 @@ +// 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 { anyString, anything, mock, when } from 'ts-mockito'; +import { DebugSession, WorkspaceFolder } from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { ConfigurationService } from '../../../../client/common/configuration/service'; +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'; + +suite('Debugging - Outdated Debugger Prompt tests.', () => { + let promptFactory: OutdatedDebuggerPromptFactory; + 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' } }, + }; + + const debugpyOutputEvent: DebugProtocol.OutputEvent = { + seq: 1, + type: 'event', + event: 'output', + body: { category: 'telemetry', output: 'debugpy', data: { packageVersion: '1.0.0' } }, + }; + + setup(() => { + const configurationService = mock(ConfigurationService); + when(configurationService.getSettings(undefined)).thenReturn(({ + experiments: { enabled: true }, + } as any) as IPythonSettings); + + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + browserLaunchStub = sinon.stub(browserApis, 'launch'); + + promptFactory = new OutdatedDebuggerPromptFactory(); + }); + + teardown(() => { + sinon.restore(); + clearTelemetryReporter(); + }); + + function createSession(workspaceFolder?: WorkspaceFolder): DebugSession { + return { + configuration: { + name: '', + request: 'launch', + type: 'python', + }, + id: 'test1', + name: 'python', + type: 'python', + workspaceFolder, + customRequest: () => Promise.resolve(), + getDebugProtocolBreakpoint: () => Promise.resolve(undefined), + }; + } + + test('Show prompt when attaching to ptvsd, more info is NOT clicked', async () => { + showInformationMessageStub.returns(Promise.resolve(undefined)); + const session = createSession(); + const prompter = await promptFactory.createDebugAdapterTracker(session); + if (prompter) { + prompter.onDidSendMessage!(ptvsdOutputEvent); + } + + browserLaunchStub.neverCalledWith(anyString()); + + // First call should show info once + + sinon.assert.calledOnce(showInformationMessageStub); + assert.ok(prompter); + + prompter!.onDidSendMessage!(ptvsdOutputEvent); + // Can't use deferred promise here + await sleep(1); + + browserLaunchStub.neverCalledWith(anyString()); + // Second time it should not be called, so overall count is one. + sinon.assert.calledOnce(showInformationMessageStub); + }); + + test('Show prompt when attaching to ptvsd, more info is clicked', async () => { + showInformationMessageStub.returns(Promise.resolve(Common.moreInfo)); + + const deferred = createDeferred(); + browserLaunchStub.callsFake(() => deferred.resolve()); + browserLaunchStub.onCall(1).callsFake(() => { + return new Promise(() => deferred.resolve()); + }); + + const session = createSession(); + const prompter = await promptFactory.createDebugAdapterTracker(session); + assert.ok(prompter); + + prompter!.onDidSendMessage!(ptvsdOutputEvent); + await deferred.promise; + + sinon.assert.calledOnce(browserLaunchStub); + + // First call should show info 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); + + sinon.assert.calledOnce(browserLaunchStub); + + // Second time it should not be called, so overall count is one. + sinon.assert.calledOnce(showInformationMessageStub); + }); + + test("Don't show prompt attaching to debugpy", async () => { + showInformationMessageStub.returns(Promise.resolve(undefined)); + + const session = createSession(); + const prompter = await promptFactory.createDebugAdapterTracker(session); + assert.ok(prompter); + + prompter!.onDidSendMessage!(debugpyOutputEvent); + // Can't use deferred promise here + await sleep(1); + + showInformationMessageStub.neverCalledWith(anything(), anything()); + }); + + const someRequest: DebugProtocol.RunInTerminalRequest = { + seq: 1, + type: 'request', + command: 'runInTerminal', + arguments: { + cwd: '', + args: [''], + }, + }; + const someEvent: DebugProtocol.ContinuedEvent = { + seq: 1, + type: 'event', + event: 'continued', + body: { threadId: 1, allThreadsContinued: true }, + }; + // Notice that this is stdout, not telemetry event. + const someOutputEvent: DebugProtocol.OutputEvent = { + seq: 1, + type: 'event', + event: 'output', + body: { category: 'stdout', output: 'ptvsd' }, + }; + + [someRequest, someEvent, someOutputEvent].forEach((message) => { + test(`Don't show prompt when non-telemetry events are seen: ${JSON.stringify(message)}`, async () => { + showInformationMessageStub.returns(Promise.resolve(undefined)); + + const session = createSession(); + const prompter = await promptFactory.createDebugAdapterTracker(session); + assert.ok(prompter); + + prompter!.onDidSendMessage!(message); + // Can't use deferred promise here + await sleep(1); + + 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 new file mode 100644 index 000000000000..e8e2cbd5d15d --- /dev/null +++ b/src/test/debugger/extension/adapter/remoteLaunchers.unit.test.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as path from 'path'; +import '../../../../client/common/extensions'; +import * as launchers from '../../../../client/debugger/extension/adapter/remoteLaunchers'; + +suite('External debugpy Debugger Launcher', () => { + [ + { + testName: 'When path to debugpy does not contains spaces', + path: path.join('path', 'to', 'debugpy'), + expectedPath: 'path/to/debugpy', + }, + { + testName: 'When path to debugpy contains spaces', + path: path.join('path', 'to', 'debugpy', 'with spaces'), + expectedPath: '"path/to/debugpy/with spaces"', + }, + ].forEach((testParams) => { + suite(testParams.testName, async () => { + test('Test remote debug launcher args (and do not wait for debugger to attach)', async () => { + const args = await launchers.getDebugpyLauncherArgs( + { + host: 'something', + port: 1234, + waitUntilDebuggerAttaches: false, + }, + testParams.path, + ); + const expectedArgs = [testParams.expectedPath, '--listen', 'something:1234']; + expect(args).to.be.deep.equal(expectedArgs); + }); + test('Test remote debug launcher args (and wait for debugger to attach)', async () => { + const args = await launchers.getDebugpyLauncherArgs( + { + host: 'something', + port: 1234, + waitUntilDebuggerAttaches: true, + }, + testParams.path, + ); + const expectedArgs = [testParams.expectedPath, '--listen', 'something:1234', '--wait-for-client']; + expect(args).to.be.deep.equal(expectedArgs); + }); + }); + }); +}); diff --git a/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts b/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts new file mode 100644 index 000000000000..4c4deb3cb9ad --- /dev/null +++ b/src/test/debugger/extension/attachQuickPick/factory.unit.test.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { anything, instance, mock, verify } from 'ts-mockito'; +import { Disposable } from 'vscode'; +import { ApplicationShell } from '../../../../client/common/application/applicationShell'; +import { CommandManager } from '../../../../client/common/application/commandManager'; +import { IApplicationShell, ICommandManager } from '../../../../client/common/application/types'; +import { Commands } from '../../../../client/common/constants'; +import { PlatformService } from '../../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { ProcessServiceFactory } from '../../../../client/common/process/processFactory'; +import { IProcessServiceFactory } from '../../../../client/common/process/types'; +import { IDisposableRegistry } from '../../../../client/common/types'; +import { AttachProcessProviderFactory } from '../../../../client/debugger/extension/attachQuickPick/factory'; + +suite('Attach to process - attach process provider factory', () => { + let applicationShell: IApplicationShell; + let commandManager: ICommandManager; + let platformService: IPlatformService; + let processServiceFactory: IProcessServiceFactory; + let disposableRegistry: IDisposableRegistry; + + let factory: AttachProcessProviderFactory; + + setup(() => { + applicationShell = mock(ApplicationShell); + commandManager = mock(CommandManager); + platformService = mock(PlatformService); + processServiceFactory = mock(ProcessServiceFactory); + disposableRegistry = []; + + factory = new AttachProcessProviderFactory( + instance(applicationShell), + instance(commandManager), + instance(platformService), + instance(processServiceFactory), + disposableRegistry, + ); + }); + + test('Register commands should not fail', () => { + factory.registerCommands(); + + verify(commandManager.registerCommand(Commands.PickLocalProcess, anything(), anything())).once(); + assert.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 new file mode 100644 index 000000000000..64d9103f3c5d --- /dev/null +++ b/src/test/debugger/extension/attachQuickPick/provider.unit.test.ts @@ -0,0 +1,459 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect } from 'chai'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; +import { PlatformService } from '../../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../../client/common/platform/types'; +import { ProcessService } from '../../../../client/common/process/proc'; +import { ProcessServiceFactory } from '../../../../client/common/process/processFactory'; +import { IProcessService, IProcessServiceFactory } from '../../../../client/common/process/types'; +import { OSType } from '../../../../client/common/utils/platform'; +import { AttachProcessProvider } from '../../../../client/debugger/extension/attachQuickPick/provider'; +import { PsProcessParser } from '../../../../client/debugger/extension/attachQuickPick/psProcessParser'; +import { IAttachItem } from '../../../../client/debugger/extension/attachQuickPick/types'; +import { WmicProcessParser } from '../../../../client/debugger/extension/attachQuickPick/wmicProcessParser'; + +suite('Attach to process - process provider', () => { + let platformService: IPlatformService; + let processService: IProcessService; + let processServiceFactory: IProcessServiceFactory; + + let provider: AttachProcessProvider; + + setup(() => { + platformService = mock(PlatformService); + processService = mock(ProcessService); + processServiceFactory = mock(ProcessServiceFactory); + when(processServiceFactory.create()).thenResolve(instance(processService)); + + provider = new AttachProcessProvider(instance(platformService), instance(processServiceFactory)); + }); + + test('The Linux process list command should be called if the platform is Linux', async () => { + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(true); + const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +1 launchd launchd +41 syslogd syslogd +146 kextd kextd +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd', + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd', + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd', + }, + ]; + when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ + stdout: psOutput, + }); + + const attachItems = await provider._getInternalProcessEntries(); + + verify( + processService.exec( + PsProcessParser.psLinuxCommand.command, + PsProcessParser.psLinuxCommand.args, + anything(), + ), + ).once(); + assert.deepEqual(attachItems, expectedOutput); + }); + + test('The macOS process list command should be called if the platform is macOS', async () => { + when(platformService.isMac).thenReturn(true); + const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +1 launchd launchd +41 syslogd syslogd +146 kextd kextd +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd', + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd', + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd', + }, + ]; + when(processService.exec(PsProcessParser.psDarwinCommand.command, anything(), anything())).thenResolve({ + stdout: psOutput, + }); + + const attachItems = await provider._getInternalProcessEntries(); + + verify( + processService.exec( + PsProcessParser.psDarwinCommand.command, + PsProcessParser.psDarwinCommand.args, + anything(), + ), + ).once(); + assert.deepEqual(attachItems, expectedOutput); + }); + + test('The Windows process list command should be called if the platform is Windows', async () => { + const windowsOutput = `CommandLine=\r +Name=System\r +ProcessId=4\r +\r +\r +CommandLine=sihost.exe\r +Name=sihost.exe\r +ProcessId=5728\r +\r +\r +CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r +Name=svchost.exe\r +ProcessId=5912\r +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe', + }, + { + label: 'svchost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + id: '5912', + processName: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + }, + ]; + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(false); + when(platformService.isWindows).thenReturn(true); + when(processService.exec(WmicProcessParser.wmicCommand.command, anything(), anything())).thenResolve({ + stdout: windowsOutput, + }); + + const attachItems = await provider._getInternalProcessEntries(); + + verify( + processService.exec(WmicProcessParser.wmicCommand.command, WmicProcessParser.wmicCommand.args, anything()), + ).once(); + assert.deepEqual(attachItems, expectedOutput); + }); + + test('An error should be thrown if the platform is neither Linux, macOS or Windows', async () => { + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(false); + when(platformService.isWindows).thenReturn(false); + when(platformService.osType).thenReturn(OSType.Unknown); + + const promise = provider._getInternalProcessEntries(); + + await expect(promise).to.eventually.be.rejectedWith(`Operating system '${OSType.Unknown}' not supported.`); + }); + + suite('POSIX getAttachItems (Linux)', () => { + setup(() => { + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(true); + }); + + test('Items returned by getAttachItems should be sorted alphabetically', async () => { + const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + 1 launchd launchd + 41 syslogd syslogd + 146 kextd kextd +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd', + }, + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd', + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd', + }, + ]; + when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ + stdout: psOutput, + }); + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + + test('Python processes should be at the top of the list returned by getAttachItems', async () => { + const psOutput = `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + 1 launchd launchd + 41 syslogd syslogd + 96 python python + 146 kextd kextd + 31896 python python script.py +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'python', + description: '96', + detail: 'python', + id: '96', + processName: 'python', + commandLine: 'python', + }, + { + label: 'python', + description: '31896', + detail: 'python script.py', + id: '31896', + processName: 'python', + commandLine: 'python script.py', + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd', + }, + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd', + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd', + }, + ]; + when(processService.exec(PsProcessParser.psLinuxCommand.command, anything(), anything())).thenResolve({ + stdout: psOutput, + }); + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + }); + + suite('Windows getAttachItems', () => { + setup(() => { + when(platformService.isMac).thenReturn(false); + when(platformService.isLinux).thenReturn(false); + when(platformService.isWindows).thenReturn(true); + }); + + test('Items returned by getAttachItems should be sorted alphabetically', async () => { + const windowsOutput = `CommandLine=\r +Name=System\r +ProcessId=4\r +\r +\r +CommandLine=\r +Name=svchost.exe\r +ProcessId=5372\r +\r +\r +CommandLine=sihost.exe\r +Name=sihost.exe\r +ProcessId=5728\r +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe', + }, + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '', + }, + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + ]; + when(processService.exec(WmicProcessParser.wmicCommand.command, anything(), anything())).thenResolve({ + stdout: windowsOutput, + }); + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + + test('Python processes should be at the top of the list returned by getAttachItems', async () => { + const windowsOutput = `CommandLine=\r +Name=System\r +ProcessId=4\r +\r +\r +CommandLine=\r +Name=svchost.exe\r +ProcessId=5372\r +\r +\r +CommandLine=sihost.exe\r +Name=sihost.exe\r +ProcessId=5728\r +\r +\r +CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r +Name=svchost.exe\r +ProcessId=5912\r +\r +\r +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r +Name=python.exe\r +ProcessId=6028\r +\r +\r +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py\r +Name=python.exe\r +ProcessId=8026\r + `; + const expectedOutput: IAttachItem[] = [ + { + label: 'python.exe', + description: '8026', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', + id: '8026', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', + }, + { + label: 'python.exe', + description: '6028', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + }, + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe', + }, + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '', + }, + { + label: 'svchost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + id: '5912', + processName: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + }, + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + ]; + when(processService.exec(WmicProcessParser.wmicCommand.command, anything(), anything())).thenResolve({ + stdout: windowsOutput, + }); + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + }); +}); diff --git a/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts b/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts new file mode 100644 index 000000000000..160c53a60c40 --- /dev/null +++ b/src/test/debugger/extension/attachQuickPick/psProcessParser.unit.test.ts @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { PsProcessParser } from '../../../../client/debugger/extension/attachQuickPick/psProcessParser'; +import { IAttachItem } from '../../../../client/debugger/extension/attachQuickPick/types'; + +suite('Attach to process - ps process parser (POSIX)', () => { + test('Processes should be parsed correctly if it is valid input', () => { + const input = `\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\ + 1 launchd launchd\n\ + 41 syslogd syslogd\n\ + 42 UserEventAgent UserEventAgent (System)\n\ + 45 uninstalld uninstalld\n\ + 146 kextd kextd\n\ +31896 python python script.py\ +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd', + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd', + }, + { + label: 'UserEventAgent', + description: '42', + detail: 'UserEventAgent (System)', + id: '42', + processName: 'UserEventAgent', + commandLine: 'UserEventAgent (System)', + }, + { + label: 'uninstalld', + description: '45', + detail: 'uninstalld', + id: '45', + processName: 'uninstalld', + commandLine: 'uninstalld', + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd', + }, + { + label: 'python', + description: '31896', + detail: 'python script.py', + id: '31896', + processName: 'python', + commandLine: 'python script.py', + }, + ]; + + const output = PsProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); + + test('Empty lines should be skipped when parsing process list input', () => { + const input = `\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\ + 1 launchd launchd\n\ + 41 syslogd syslogd\n\ + 42 UserEventAgent UserEventAgent (System)\n\ +\n\ + 146 kextd kextd\n\ + 31896 python python script.py\ +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd', + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd', + }, + { + label: 'UserEventAgent', + description: '42', + detail: 'UserEventAgent (System)', + id: '42', + processName: 'UserEventAgent', + commandLine: 'UserEventAgent (System)', + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd', + }, + { + label: 'python', + description: '31896', + detail: 'python script.py', + id: '31896', + processName: 'python', + commandLine: 'python script.py', + }, + ]; + + const output = PsProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); + + test('Incorrectly formatted lines should be skipped when parsing process list input', () => { + const input = `\ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\ + 1 launchd launchd\n\ + 41 syslogd syslogd\n\ + 42 UserEventAgent UserEventAgent (System)\n\ + 45 uninstalld uninstalld\n\ + 146 kextd kextd\n\ + 31896 python python script.py\ +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'launchd', + description: '1', + detail: 'launchd', + id: '1', + processName: 'launchd', + commandLine: 'launchd', + }, + { + label: 'syslogd', + description: '41', + detail: 'syslogd', + id: '41', + processName: 'syslogd', + commandLine: 'syslogd', + }, + { + label: 'UserEventAgent', + description: '42', + detail: 'UserEventAgent (System)', + id: '42', + processName: 'UserEventAgent', + commandLine: 'UserEventAgent (System)', + }, + { + label: 'kextd', + description: '146', + detail: 'kextd', + id: '146', + processName: 'kextd', + commandLine: 'kextd', + }, + { + label: 'python', + description: '31896', + detail: 'python script.py', + id: '31896', + processName: 'python', + commandLine: 'python script.py', + }, + ]; + + const output = PsProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); +}); diff --git a/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts b/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts new file mode 100644 index 000000000000..e29490c47926 --- /dev/null +++ b/src/test/debugger/extension/attachQuickPick/wmicProcessParser.unit.test.ts @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { IAttachItem } from '../../../../client/debugger/extension/attachQuickPick/types'; +import { WmicProcessParser } from '../../../../client/debugger/extension/attachQuickPick/wmicProcessParser'; + +suite('Attach to process - wmic process parser (Windows)', () => { + test('Processes should be parsed correctly if it is valid input', () => { + const input = ` +CommandLine=\r\n\ +Name=System\r\n\ +ProcessId=4\r\n\ +\r\n\ +\r\n\ +CommandLine=\r\n\ +Name=svchost.exe\r\n\ +ProcessId=5372\r\n\ +\r\n\ +\r\n\ +CommandLine=sihost.exe\r\n\ +Name=sihost.exe\r\n\ +ProcessId=5728\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r\n\ +Name=svchost.exe\r\n\ +ProcessId=5912\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r\n\ +Name=python.exe\r\n\ +ProcessId=6028\r\n\ +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '', + }, + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe', + }, + { + label: 'svchost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + id: '5912', + processName: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + }, + { + label: 'python.exe', + description: '6028', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + }, + ]; + + const output = WmicProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); + + test('Incorrectly formatted lines should be skipped when parsing process list input', () => { + const input = ` +CommandLine=\r\n\ +Name=System\r\n\ +ProcessId=4\r\n\ +\r\n\ +\r\n\ +CommandLine=\r\n\ +Name=svchost.exe\r\n\ +ProcessId=5372\r\n\ +\r\n\ +\r\n\ +CommandLine=sihost.exe\r\n\ +Name=sihost.exe\r\n\ +ProcessId=5728\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r\n\ +Name=svchost.exe\r\n\ +IncorrectKey=shouldnt.be.here\r\n\ +ProcessId=5912\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r\n\ +Name=python.exe\r\n\ +ProcessId=6028\r\n\ +`; + + const expectedOutput: IAttachItem[] = [ + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '', + }, + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe', + }, + { + label: 'svchost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + id: '5912', + processName: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + }, + { + label: 'python.exe', + description: '6028', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + }, + ]; + + const output = WmicProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); + + test('Command lines starting with a DOS device path prefix should be parsed correctly', () => { + const input = ` +CommandLine=\r\n\ +Name=System\r\n\ +ProcessId=4\r\n\ +\r\n\ +\r\n\ +CommandLine=\\??\\C:\\WINDOWS\\system32\\conhost.exe\r\n\ +Name=conhost.exe\r\n\ +ProcessId=5912\r\n\ +\r\n\ +\r\n\ +CommandLine=C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py\r\n\ +Name=python.exe\r\n\ +ProcessId=6028\r\n\ +`; + + const expectedOutput: IAttachItem[] = [ + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + { + label: 'conhost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\conhost.exe', + id: '5912', + processName: 'conhost.exe', + commandLine: 'C:\\WINDOWS\\system32\\conhost.exe', + }, + { + label: 'python.exe', + description: '6028', + detail: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + }, + ]; + + const output = WmicProcessParser.parseProcesses(input); + + assert.deepEqual(output, expectedOutput); + }); +}); diff --git a/src/test/debugger/extension/banner.unit.test.ts b/src/test/debugger/extension/banner.unit.test.ts deleted file mode 100644 index c80564c86ac2..000000000000 --- a/src/test/debugger/extension/banner.unit.test.ts +++ /dev/null @@ -1,272 +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, - ILogger, 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; - let browser: typemoq.IMock; - let launchCounterState: typemoq.IMock>; - let launchThresholdCounterState: typemoq.IMock>; - let showBannerState: typemoq.IMock>; - let userSelected: boolean | undefined; - let userSelectedState: typemoq.IMock>; - let debugService: typemoq.IMock; - let appShell: typemoq.IMock; - let runtime: typemoq.IMock; - 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(); - browser = typemoq.Mock.ofType(); - debugService = typemoq.Mock.ofType(); - const logger = typemoq.Mock.ofType(); - - launchCounterState = typemoq.Mock.ofType>(); - showBannerState = typemoq.Mock.ofType>(); - appShell = typemoq.Mock.ofType(); - runtime = typemoq.Mock.ofType(); - launchThresholdCounterState = typemoq.Mock.ofType>(); - userSelected = true; - userSelectedState = typemoq.Mock.ofType>(); - const factory = typemoq.Mock.ofType(); - 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(ILogger))).returns(() => logger.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; - 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; - 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; - 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 24dc5c67d169..ae13ad375371 100644 --- a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts +++ b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts @@ -3,152 +3,71 @@ 'use strict'; -// tslint:disable:no-any - import { expect } from 'chai'; -import * as path from 'path'; -import { instance, mock, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; -import { FileSystem } from '../../../../client/common/platform/fileSystem'; -import { IFileSystem } from '../../../../client/common/platform/types'; -import { IMultiStepInput, IMultiStepInputFactory } from '../../../../client/common/utils/multiStepInput'; -import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +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>; let launchResolver: typemoq.IMock>; let configService: TestPythonDebugConfigurationService; - let multiStepFactory: typemoq.IMock; - let providerFactory: DebugConfigurationProviderFactory; - let fs: IFileSystem; - class TestPythonDebugConfigurationService extends PythonDebugConfigurationService { - // tslint:disable-next-line:no-unnecessary-override - public async pickDebugConfiguration(input: IMultiStepInput, state: DebugConfigurationState) { - return super.pickDebugConfiguration(input, state); - } - } + class TestPythonDebugConfigurationService extends PythonDebugConfigurationService {} setup(() => { attachResolver = typemoq.Mock.ofType>(); launchResolver = typemoq.Mock.ofType>(); - multiStepFactory = typemoq.Mock.ofType(); - providerFactory = mock(DebugConfigurationProviderFactory); - fs = mock(FileSystem); - configService = new TestPythonDebugConfigurationService(attachResolver.object, launchResolver.object, instance(providerFactory), multiStepFactory.object, - instance(fs)); + 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; + const config = ({ + 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())) - .returns(() => Promise.resolve(expectedConfig as any)) + .setup((a) => + a.resolveDebugConfiguration(typemoq.It.isValue(folder), typemoq.It.isValue(config), typemoq.It.isAny()), + ) + .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())) + .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(); }); - [ - { request: 'launch' }, { request: undefined } - ].forEach(config => { + [{ request: 'launch' }, { request: undefined }].forEach((config) => { test(`Should use launch resolver when passing launch config with request=${config.request}`, async () => { const folder = { name: '1', index: 0, uri: Uri.parse('1234') }; const expectedConfig = { yay: 1 }; launchResolver - .setup(a => a.resolveDebugConfiguration(typemoq.It.isValue(folder), typemoq.It.isValue(config as any as LaunchRequestArguments), typemoq.It.isAny())) - .returns(() => Promise.resolve(expectedConfig as any)) + .setup((a) => + a.resolveDebugConfiguration( + typemoq.It.isValue(folder), + typemoq.It.isValue((config as DebugConfiguration) as LaunchRequestArguments), + typemoq.It.isAny(), + ), + ) + .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())) + .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>(); - 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>(); - 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: (_, state) => { - 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 default config is returned', async () => { - const expectedConfig = { yes: 'Updated' }; - const multiStepInput = { - run: () => Promise.resolve() - }; - multiStepFactory - .setup(f => f.create()) - .returns(() => multiStepInput as any) - .verifiable(typemoq.Times.once()); - const jsFile = path.join(EXTENSION_ROOT_DIR, 'resources', 'default.launch.json'); - when(fs.readFile(jsFile)).thenResolve(JSON.stringify([expectedConfig])); - const config = await configService.provideDebugConfigurations!({} as any); - - multiStepFactory.verifyAll(); - expect(config).to.deep.equal([expectedConfig]); - }); }); 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/providers/djangoLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/djangoLaunch.unit.test.ts deleted file mode 100644 index 18098627d4f8..000000000000 --- a/src/test/debugger/extension/configuration/providers/djangoLaunch.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: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 { localize } 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; - 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 { - return super.getManagePyPath(folder); - } - } - setup(() => { - fs = mock(FileSystem); - workspaceService = mock(WorkspaceService); - pathUtils = mock(PathUtils); - input = mock>(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: localize('python.snippet.launch.django.label', 'Python: Django')(), - type: DebuggerTypeName, - request: 'launch', - program: 'xyz.py', - args: [ - 'runserver', - '--noreload', - '--nothreading' - ], - 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: localize('python.snippet.launch.django.label', 'Python: Django')(), - type: DebuggerTypeName, - request: 'launch', - program: 'hello', - args: [ - 'runserver', - '--noreload', - '--nothreading' - ], - 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: localize('python.snippet.launch.django.label', 'Python: Django')(), - type: DebuggerTypeName, - request: 'launch', - program: defaultProgram, - args: [ - 'runserver', - '--noreload', - '--nothreading' - ], - 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 1e06afe48dc7..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 { localize } 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: localize('python.snippet.launch.standard.label', 'Python: Current File')(), - 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 ee4e1e665fc0..000000000000 --- a/src/test/debugger/extension/configuration/providers/flaskLaunch.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 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 { localize } 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; - class TestFlaskLaunchDebugConfigurationProvider extends FlaskLaunchDebugConfigurationProvider { - // tslint:disable-next-line:no-unnecessary-override - public async getApplicationPath(folder: WorkspaceFolder): Promise { - return super.getApplicationPath(folder); - } - } - setup(() => { - fs = mock(FileSystem); - input = mock>(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: localize('python.snippet.launch.flask.label', 'Python: Flask')(), - 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: localize('python.snippet.launch.flask.label', 'Python: Flask')(), - 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: localize('python.snippet.launch.flask.label', 'Python: Flask')(), - 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 754516a388de..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 { localize } 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); - - when(input.showInputBox(anything())).thenResolve(); - - await provider.buildConfiguration(instance(input), state); - - const config = { - name: localize('python.snippet.launch.module.label', 'Python: Module')(), - type: DebuggerTypeName, - request: 'launch', - module: 'enter-your-module-name' - }; - - 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); - - when(input.showInputBox(anything())).thenResolve('hello'); - - await provider.buildConfiguration(instance(input), state); - - const config = { - name: localize('python.snippet.launch.module.label', 'Python: Module')(), - type: DebuggerTypeName, - request: 'launch', - module: 'hello' - }; - - 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 bcbad4d04c45..000000000000 --- a/src/test/debugger/extension/configuration/providers/providerFactory.unit.test.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 { 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; - let factory: IDebugConfigurationProviderFactory; - setup(() => { - mappedProviders = new Map(); - getNamesAndValues(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)! - ); - }); - getNamesAndValues(DebugConfigurationType).forEach(item => { - test(`Configuration Provider for ${item.name}`, function () { - if (item.value === DebugConfigurationType.default) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - 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 b3d93ff4b39b..000000000000 --- a/src/test/debugger/extension/configuration/providers/pyramidLaunch.unit.test.ts +++ /dev/null @@ -1,193 +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 { localize } 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; - 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 { - return super.getDevelopmentIniPath(folder); - } - } - setup(() => { - fs = mock(FileSystem); - workspaceService = mock(WorkspaceService); - pathUtils = mock(PathUtils); - input = mock>(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: localize('python.snippet.launch.pyramid.label', 'Python: Pyramid Application')(), - type: DebuggerTypeName, - request: 'launch', - 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: localize('python.snippet.launch.pyramid.label', 'Python: Pyramid Application')(), - type: DebuggerTypeName, - request: 'launch', - 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: localize('python.snippet.launch.pyramid.label', 'Python: Pyramid Application')(), - type: DebuggerTypeName, - request: 'launch', - 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 b91f815b5e57..000000000000 --- a/src/test/debugger/extension/configuration/providers/remoteAttach.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 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 { localize } 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; - class TestRemoteAttachDebugConfigurationProvider extends RemoteAttachDebugConfigurationProvider { - // tslint:disable-next-line:no-unnecessary-override - public async configurePort(i: MultiStepInput, config: Partial) { - return super.configurePort(i, config); - } - } - setup(() => { - input = mock>(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: { port?: number } = {}; - when(input.showInputBox(anything())).thenResolve('xyz'); - - await provider.configurePort(instance(input), config); - - verify(input.showInputBox(anything())).once(); - expect(config.port).to.equal(5678); - }); - test('Configure port will default to 5678', async () => { - const config: { port?: number } = {}; - when(input.showInputBox(anything())).thenResolve(); - - await provider.configurePort(instance(input), config); - - verify(input.showInputBox(anything())).once(); - expect(config.port).to.equal(5678); - }); - test('Configure port will use user selected value', async () => { - const config: { port?: number } = {}; - when(input.showInputBox(anything())).thenResolve('1234'); - - await provider.configurePort(instance(input), config); - - verify(input.showInputBox(anything())).once(); - expect(config.port).to.equal(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: localize('python.snippet.launch.attach.label', 'Python: Attach')(), - type: DebuggerTypeName, - request: 'attach', - port: 5678, - host: 'localhost' - }; - - 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.port = 9999; - return Promise.resolve(); - }; - - const configurePort = await provider.buildConfiguration(instance(input), state); - if (configurePort) { - await configurePort(input, state); - } - - const config = { - name: localize('python.snippet.launch.attach.label', 'Python: Attach')(), - type: DebuggerTypeName, - request: 'attach', - port: 9999, - host: 'Hello' - }; - - 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 f4f1d36fe8de..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,142 +3,224 @@ '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 path from 'path'; 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 { getNamesAndValues } from '../../../../../client/common/utils/enum'; -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'; - -getNamesAndValues(OSType).forEach(os => { - if (os.value === OSType.Unknown) { +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 === platform.OSType.Unknown) { return; } - suite(`Debugging - Config Resolver attach, OS = ${os.name}`, () => { - let serviceContainer: TypeMoq.IMock; + + function getAvailableOptions(): string[] { + const options = [DebugOptions.RedirectOutput]; + if (osType === platform.OSType.Windows) { + options.push(DebugOptions.FixFilePathCase); + } + options.push(DebugOptions.ShowReturnValue); + + return options; + } + + suite(`Debugging - Config Resolver attach, OS = ${osName}`, () => { let debugProvider: DebugConfigurationProvider; - let platformService: TypeMoq.IMock; - let fileSystem: TypeMoq.IMock; - let documentManager: TypeMoq.IMock; let configurationService: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - const debugOptionsAvailable = [DebugOptions.RedirectOutput]; - if (os.value === OSType.Windows) { - debugOptionsAvailable.push(DebugOptions.FixFilePathCase); - debugOptionsAvailable.push(DebugOptions.WindowsClient); - } else { - debugOptionsAvailable.push(DebugOptions.UnixClient); - } + let interpreterService: TypeMoq.IMock; + let getActiveTextEditorStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + let getOSTypeStub: sinon.SinonStub; + const debugOptionsAvailable = getAvailableOptions(); + setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - platformService = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); configurationService = TypeMoq.Mock.ofType(); - fileSystem = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); - platformService.setup(p => p.isWindows).returns(() => os.value === OSType.Windows); - platformService.setup(p => p.isMac).returns(() => os.value === OSType.OSX); - platformService.setup(p => p.isLinux).returns(() => os.value === OSType.Linux); - documentManager = TypeMoq.Mock.ofType(); - debugProvider = new AttachConfigurationResolver(workspaceService.object, documentManager.object, platformService.object, configurationService.object); + interpreterService = TypeMoq.Mock.ofType(); + 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(); - folder.setup(f => f.uri).returns(() => Uri.file(folderPath)); + 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(); const document = TypeMoq.Mock.ofType(); - 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); + document.setup((d) => d.languageId).returns(() => languageId); + document.setup((d) => d.fileName).returns(() => fileName); + textEditor.setup((t) => t.document).returns(() => document.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 = { + name: 'Python attach', + type: 'python', + request: 'attach', + }; + + async function resolveDebugConfiguration( + workspaceFolder: WorkspaceFolder | undefined, + attachConfig: Partial, + ) { + 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('Ensure \'localRoot\' is left unaltered', async () => { + + test('Default host should not be added if connect is available.', async () => { + const pythonFile = 'xyz.py'; + + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupWorkspaces([]); + + 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 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); setupActiveEditor(activeFile, PYTHON_LANGUAGE); @@ -146,11 +228,15 @@ getNamesAndValues(OSType).forEach(os => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, request: 'attach' } as any as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + }); expect(debugConfig).to.have.property('localRoot', localRoot); }); - ['localhost', '127.0.0.1', '::1'].forEach(host => { + + ['localhost', 'LOCALHOST', '127.0.0.1', '::1'].forEach((host) => { test(`Ensure path mappings are automatically added when host is '${host}'`, async () => { const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); @@ -159,16 +245,149 @@ getNamesAndValues(OSType).forEach(os => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, host, request: 'attach' } as any as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + host, + }); 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 (platform.getOSType() !== platform.OSType.Windows || osType !== platform.OSType.Windows) { + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + host, + }); + 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 (platform.getOSType() === platform.OSType.Windows || osType === platform.OSType.Windows) { + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + host, + }); + 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 (platform.getOSType() !== platform.OSType.Windows || osType !== platform.OSType.Windows) { + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugPathMappings = [ + { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' }, + ]; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + pathMappings: debugPathMappings, + host, + }); + 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 (platform.getOSType() === platform.OSType.Windows || osType === platform.OSType.Windows) { + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugPathMappings = [ + { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' }, + ]; + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + pathMappings: debugPathMappings, + host, + }); + const { pathMappings } = debugConfig as AttachRequestArguments; + + const expected = Uri.file(path.join('USR', 'Debug', 'Python_Path', localRoot)).fsPath; + 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')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + host, + }); + 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 => { + + ['192.168.1.123', 'don.debugger.com'].forEach((host) => { test(`Ensure path mappings are not automatically added when host is '${host}'`, async () => { const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); @@ -177,14 +396,19 @@ getNamesAndValues(OSType).forEach(os => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, host, request: 'attach' } as any as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + host, + }); expect(debugConfig).to.have.property('localRoot', localRoot); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; - expect(pathMappings).to.be.lengthOf(0); + const { pathMappings } = debugConfig as AttachRequestArguments; + expect(pathMappings || []).to.be.lengthOf(0); }); }); - test('Ensure \'localRoot\' and \'remoteRoot\' is used', async () => { + + test("Ensure 'localRoot' and 'remoteRoot' is used", async () => { const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); setupActiveEditor(activeFile, PYTHON_LANGUAGE); @@ -193,12 +417,17 @@ getNamesAndValues(OSType).forEach(os => { const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, remoteRoot, request: 'attach' } as any as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + remoteRoot, + }); expect(debugConfig!.pathMappings).to.be.lengthOf(1); expect(debugConfig!.pathMappings).to.deep.include({ localRoot, remoteRoot }); }); - test('Ensure \'localRoot\' and \'remoteRoot\' is used', async () => { + + test("Ensure 'localRoot' and 'remoteRoot' is used", async () => { const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); setupActiveEditor(activeFile, PYTHON_LANGUAGE); @@ -207,12 +436,17 @@ getNamesAndValues(OSType).forEach(os => { const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, remoteRoot, request: 'attach' } as any as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + localRoot, + remoteRoot, + }); expect(debugConfig!.pathMappings).to.be.lengthOf(1); expect(debugConfig!.pathMappings).to.deep.include({ localRoot, remoteRoot }); }); - test('Ensure \'remoteRoot\' is left unaltered', async () => { + + test("Ensure 'remoteRoot' is left unaltered", async () => { const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); setupActiveEditor(activeFile, PYTHON_LANGUAGE); @@ -220,11 +454,15 @@ getNamesAndValues(OSType).forEach(os => { setupWorkspaces([defaultWorkspace]); const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { remoteRoot, request: 'attach' } as any as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + remoteRoot, + }); expect(debugConfig).to.have.property('remoteRoot', remoteRoot); }); - test('Ensure \'port\' is left unaltered', async () => { + + test("Ensure 'port' is left unaltered", async () => { const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); setupActiveEditor(activeFile, PYTHON_LANGUAGE); @@ -232,21 +470,30 @@ getNamesAndValues(OSType).forEach(os => { setupWorkspaces([defaultWorkspace]); const port = 12341234; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { port, request: 'attach' } as any as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + port, + }); expect(debugConfig).to.have.property('port', port); }); - test('Ensure \'debugOptions\' are left unaltered', async () => { + test("Ensure 'debugOptions' are left unaltered", async () => { const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(__dirname); setupActiveEditor(activeFile, PYTHON_LANGUAGE); const defaultWorkspace = path.join('usr', 'desktop'); setupWorkspaces([defaultWorkspace]); - const debugOptions = debugOptionsAvailable.slice().concat(DebugOptions.Jinja, DebugOptions.Sudo); + const debugOptions = debugOptionsAvailable + .slice() + .concat(DebugOptions.Jinja, DebugOptions.Sudo) as DebugOptions[]; const expectedDebugOptions = debugOptions.slice(); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { debugOptions, request: 'attach' } as any as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...attach, + debugOptions, + }); + expect(debugConfig).to.have.property('clientOS', getClientOS()); expect(debugConfig).to.have.property('debugOptions').to.be.deep.equal(expectedDebugOptions); }); }); 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 b977734bfb35..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,93 +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 { 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 { - public resolveDebugConfiguration(_folder: WorkspaceFolder | undefined, _debugConfiguration: DebugConfiguration, _token?: CancellationToken): Promise { + public resolveDebugConfiguration( + _folder: WorkspaceFolder | undefined, + _debugConfiguration: DebugConfiguration, + _token?: CancellationToken, + ): Promise { 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 { + 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); + + public resolveAndUpdatePythonPath( + 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) { + return BaseConfigurationResolver.isDebuggingFastAPI(debugConfiguration); + } + public isDebuggingFlask(debugConfiguration: Partial) { - return super.isDebuggingFlask(debugConfiguration); + return BaseConfigurationResolver.isDebuggingFlask(debugConfiguration); } } let resolver: BaseResolver; - let workspaceService: IWorkspaceService; - 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); configurationService = mock(ConfigurationService); - resolver = new BaseResolver(instance(workspaceService), instance(documentManager), instance(configurationService)); + interpreterService = mock(); + 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 filepath of active editor if file is python', () => { - const expectedFileName = 'my.py'; - const editor = typemoq.Mock.ofType(); - const doc = typemoq.Mock.ofType(); - - 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); + teardown(() => { + sinon.restore(); }); - test('Program should return undefined if active file is not python', () => { - const editor = typemoq.Mock.ofType(); - const doc = typemoq.Mock.ofType(); - - editor.setup(e => e.document).returns(() => doc.object).verifiable(typemoq.Times.once()); - doc.setup(d => d.languageId).returns(() => 'C#').verifiable(typemoq.Times.once()); - when(documentManager.activeTextEditor).thenReturn(editor.object); - - const program = resolver.getProgram(); - expect(program).to.be.equal(undefined, 'Not undefined'); - }); - test('Program should return undefined if there is no active editor', () => { - when(documentManager.activeTextEditor).thenReturn(undefined); - - const program = resolver.getProgram(); - - expect(program).to.be.equal(undefined, 'Not undefined'); - }); test('Should get workspace folder when workspace folder is provided', () => { const expectedUri = Uri.parse('mock'); const folder: WorkspaceFolder = { index: 0, uri: expectedUri, name: 'mock' }; @@ -97,28 +92,33 @@ suite('Debugging - Config Resolver', () => { expect(uri).to.be.deep.equal(expectedUri); }); [ - { title: 'Should get directory of active program when there are not workspace folders', workspaceFolders: undefined }, - { title: 'Should get directory of active program when there are 0 workspace folders', workspaceFolders: [] } - ] - .forEach(item => { - test(item.title, () => { - const programPath = path.join('one', 'two', 'three.xyz'); + { + title: 'Should get directory of active program when there are not workspace folders', + workspaceFolders: undefined, + }, + { title: 'Should get directory of active program when there are 0 workspace folders', workspaceFolders: [] }, + ].forEach((item) => { + test(item.title, () => { + const programPath = path.join('one', 'two', 'three.xyz'); - resolver.getProgram = () => programPath; - when(workspaceService.workspaceFolders).thenReturn(item.workspaceFolders); + getProgramStub.returns(programPath); + getWorkspaceFoldersStub.returns(item.workspaceFolders); - const uri = resolver.getWorkspaceFolder(undefined); + const uri = resolver.getWorkspaceFolder(undefined); - expect(uri!.fsPath).to.be.deep.equal(Uri.file(path.dirname(programPath)).fsPath); - }); + expect(uri!.fsPath).to.be.deep.equal(Uri.file(path.dirname(programPath)).fsPath); }); + }); test('Should return uri of workspace folder if there is only one workspace folder', () => { const expectedUri = Uri.parse('mock'); const folder: WorkspaceFolder = { index: 0, uri: expectedUri, name: 'mock' }; const folders: WorkspaceFolder[] = [folder]; - resolver.getProgram = () => undefined; - when(workspaceService.workspaceFolders).thenReturn(folders); + getProgramStub.returns(undefined); + + getWorkspaceFolderStub.returns(folder); + + getWorkspaceFoldersStub.returns(folders); const uri = resolver.getWorkspaceFolder(undefined); @@ -130,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); @@ -144,61 +146,192 @@ 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 ${config:python.pythonPath}', () => { + test('python in debug config must point to pythonPath in settings if pythonPath in config is ${command:python.interpreterPath}', async () => { const config = { - pythonPath: '${config:python.pythonPath}' + 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'); - resolver.resolveAndUpdatePythonPath(undefined, config as any); + 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', pythonPath); + expect(config).to.have.property('debugLauncherPython', pythonPath); }); - const localHostTestMatrix = { localhost: true, '127.0.0.1': true, '::1': true, '127.0.0.2': false, '156.1.2.3': false, '::2': false }; - Object.keys(localHostTestMatrix) - .forEach(key => { - test(`Local host = ${localHostTestMatrix[key]} for ${key}`, () => { - const isLocalHost = resolver.isLocalHost(key); - expect(isLocalHost).to.equal(localHostTestMatrix[key]); - }); + test('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'); + + const config = { debugAdapterPython: debugAdapterPythonPath, debugLauncherPython: debugLauncherPythonPath }; + 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', 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 = { + localhost: true, + '127.0.0.1': true, + '::1': true, + '127.0.0.2': false, + '156.1.2.3': false, + '::2': false, + }; + Object.keys(localHostTestMatrix).forEach((key) => { + test(`Local host = ${localHostTestMatrix[key]} for ${key}`, () => { + const isLocalHost = resolver.isLocalHost(key); + + expect(isLocalHost).to.equal(localHostTestMatrix[key]); }); + }); + test('Is debugging 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 new file mode 100644 index 000000000000..24c0599a04a6 --- /dev/null +++ b/src/test/debugger/extension/configuration/resolvers/common.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { getNamesAndValues } from '../../../../../client/common/utils/enum'; +import { OSType, getOSType } from '../../../../../client/common/utils/platform'; + +const OS_TYPE = getOSType(); + +interface IPathModule { + sep: string; + dirname(path: string): string; + join(...paths: string[]): string; +} + +// The set of information, related to a target OS, that are available +// to tests. The target OS is not necessarily the native OS. +type OSTestInfo = [ + string, // os name + OSType, + IPathModule, +]; + +// For each supported OS, provide a set of helpers to use in tests. +export function getInfoPerOS(): OSTestInfo[] { + return getNamesAndValues(OSType).map((os) => { + const osType = os.value as OSType; + return [os.name, osType, getPathModuleForOS(osType)]; + }); +} + +// Decide which "path" module to use. +// By default we use the regular module. +function getPathModuleForOS(osType: OSType): IPathModule { + if (osType === OS_TYPE) { + return path; + } + + // We are testing a different OS from the native one. + // So use a "path" module matching the target OS. + return osType === OSType.Windows ? path.win32 : path.posix; +} 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(); + const doc = typemoq.Mock.ofType(); + + 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(); + const doc = typemoq.Mock.ofType(); + + 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 2b07d892d5c5..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,478 +3,1144 @@ '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 path from 'path'; import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; import { DebugConfiguration, DebugConfigurationProvider, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; -import { InvalidPythonPathInDebuggerServiceId } from '../../../../../client/application/diagnostics/checks/invalidPythonPathInDebugger'; -import { IDiagnosticsService, IInvalidPythonPathInDebuggerService } from '../../../../../client/application/diagnostics/types'; -import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../../../../client/common/application/types'; +import { IInvalidPythonPathInDebuggerService } from '../../../../../client/application/diagnostics/types'; import { PYTHON_LANGUAGE } from '../../../../../client/common/constants'; -import { IFileSystem, IPlatformService } from '../../../../../client/common/platform/types'; import { IPythonExecutionFactory, IPythonExecutionService } from '../../../../../client/common/process/types'; -import { IConfigurationService, ILogger, IPythonSettings } from '../../../../../client/common/types'; +import { IConfigurationService, IPythonSettings } from '../../../../../client/common/types'; import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { ConfigurationProviderUtils } from '../../../../../client/debugger/extension/configuration/configurationProviderUtils'; +import { IDebugEnvironmentVariablesService } from '../../../../../client/debugger/extension/configuration/resolvers/helper'; import { LaunchConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/launch'; -import { IConfigurationProviderUtils } from '../../../../../client/debugger/extension/configuration/types'; -import { DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; -import { IInterpreterHelper } from '../../../../../client/interpreter/contracts'; -import { IServiceContainer } from '../../../../../client/ioc/types'; - -suite('Debugging - Config Resolver Launch', () => { - let serviceContainer: TypeMoq.IMock; - let debugProvider: DebugConfigurationProvider; - let platformService: TypeMoq.IMock; - let fileSystem: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let pythonExecutionService: TypeMoq.IMock; - let logger: TypeMoq.IMock; - let helper: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let documentManager: TypeMoq.IMock; - let diagnosticsService: TypeMoq.IMock; - function createMoqWorkspaceFolder(folderPath: string) { - const folder = TypeMoq.Mock.ofType(); - folder.setup(f => f.uri).returns(() => Uri.file(folderPath)); - return folder.object; - } - function setupIoc(pythonPath: string, isWindows: boolean = false, isMac: boolean = false, isLinux: boolean = false) { - const confgService = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); - documentManager = TypeMoq.Mock.ofType(); - serviceContainer = TypeMoq.Mock.ofType(); - - platformService = TypeMoq.Mock.ofType(); - fileSystem = TypeMoq.Mock.ofType(); - appShell = TypeMoq.Mock.ofType(); - logger = TypeMoq.Mock.ofType(); - diagnosticsService = TypeMoq.Mock.ofType(); - - pythonExecutionService = TypeMoq.Mock.ofType(); - helper = TypeMoq.Mock.ofType(); - pythonExecutionService.setup((x: any) => x.then).returns(() => undefined); - const factory = TypeMoq.Mock.ofType(); - factory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(pythonExecutionService.object)); - helper.setup(h => h.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); - diagnosticsService - .setup(h => h.validatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)); - - const configProviderUtils = new ConfigurationProviderUtils(factory.object, fileSystem.object, appShell.object); - - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPythonExecutionFactory))).returns(() => factory.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => confgService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.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(IConfigurationProviderUtils))).returns(() => configProviderUtils); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILogger))).returns(() => logger.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDiagnosticsService), TypeMoq.It.isValue(InvalidPythonPathInDebuggerServiceId))).returns(() => diagnosticsService.object); - - const settings = TypeMoq.Mock.ofType(); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - confgService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - setupOs(isWindows, isMac, isLinux); - - debugProvider = new LaunchConfigurationResolver(workspaceService.object, documentManager.object, configProviderUtils, diagnosticsService.object, platformService.object, confgService.object); +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 === platform.OSType.Unknown) { + return; } - function setupActiveEditor(fileName: string | undefined, languageId: string) { - if (fileName) { - const textEditor = TypeMoq.Mock.ofType(); - const document = TypeMoq.Mock.ofType(); - document.setup(d => d.languageId).returns(() => languageId); - document.setup(d => d.fileName).returns(() => fileName); - textEditor.setup(t => t.document).returns(() => document.object); - documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); - } else { - documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + + suite(`Debugging - Config Resolver Launch, OS = ${osName}`, () => { + let debugProvider: DebugConfigurationProvider; + let pythonExecutionService: TypeMoq.IMock; + let helper: TypeMoq.IMock; + const envVars = { FOO: 'BAR' }; + + let diagnosticsService: TypeMoq.IMock; + let configService: TypeMoq.IMock; + let debugEnvHelper: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let environmentActivationService: TypeMoq.IMock; + 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(); + folder.setup((f) => f.uri).returns(() => Uri.file(folderPath)); + return folder.object; } - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager))).returns(() => documentManager.object); - } - function setupWorkspaces(folders: string[]) { - const workspaceFolders = folders.map(createMoqWorkspaceFolder); - workspaceService.setup(w => w.workspaceFolders).returns(() => workspaceFolders); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - } - function setupOs(isWindows: boolean, isMac: boolean, isLinux: boolean) { - platformService.setup(p => p.isWindows).returns(() => isWindows); - platformService.setup(p => p.isMac).returns(() => isMac); - platformService.setup(p => p.isLinux).returns(() => isLinux); - } - 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); - - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env').toLowerCase()); - expect(debugConfig).to.have.property('env'); - // tslint:disable-next-line:no-any - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); - }); - test('Defaults should be returned when an object with \'noDebug\' property is passed with a Workspace Folder and active file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { noDebug: true } as any as DebugConfiguration); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env').toLowerCase()); - expect(debugConfig).to.have.property('env'); - // tslint:disable-next-line:no-any - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); - }); - test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - setupWorkspaces([]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); - const filePath = Uri.file(path.dirname('')).fsPath; - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env').toLowerCase()); - expect(debugConfig).to.have.property('env'); - // tslint:disable-next-line:no-any - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); - }); - test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - setupIoc(pythonPath); - setupActiveEditor(undefined, PYTHON_LANGUAGE); - setupWorkspaces([]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('program', ''); - expect(debugConfig).not.to.have.property('cwd'); - expect(debugConfig).not.to.have.property('envFile'); - expect(debugConfig).to.have.property('env'); - // tslint:disable-next-line:no-any - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); - }); - test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and non python file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.js'; - setupIoc(pythonPath); - setupActiveEditor(activeFile, 'javascript'); - setupWorkspaces([]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('program', ''); - expect(debugConfig).not.to.have.property('cwd'); - expect(debugConfig).not.to.have.property('envFile'); - expect(debugConfig).to.have.property('env'); - // tslint:disable-next-line:no-any - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); - }); - test('Defaults should be returned when an empty object is passed without Workspace Folder, with a workspace and an active python file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); - const filePath = Uri.file(defaultWorkspace).fsPath; - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('program', activeFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env').toLowerCase()); - expect(debugConfig).to.have.property('env'); - // tslint:disable-next-line:no-any - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); - }); - test('Ensure `${config:python.pythonPath}` is replaced with actual pythonPath', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupIoc(pythonPath); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { pythonPath: '${config:python.pythonPath}' } as any as DebugConfiguration); - - expect(debugConfig).to.have.property('pythonPath', pythonPath); - }); - test('Ensure hardcoded pythonPath is left unaltered', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupIoc(pythonPath); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugPythonPath = `Debug_PythonPath_${new Date().toString()}`; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { pythonPath: debugPythonPath } as any as DebugConfiguration); - - expect(debugConfig).to.have.property('pythonPath', debugPythonPath); - }); - test('Test defaults of debugger', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); - - expect(debugConfig).to.have.property('console', 'integratedTerminal'); - expect(debugConfig).to.have.property('stopOnEntry', false); - expect(debugConfig).to.have.property('showReturnValue', false); - expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).to.be.deep.equal([DebugOptions.RedirectOutput]); - }); - test('Test defaults of python debugger', async () => { - if ('python' === DebuggerTypeName) { - return; + + function getClientOS() { + return osType === platform.OSType.Windows ? 'windows' : 'unix'; } - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); - - expect(debugConfig).to.have.property('stopOnEntry', false); - expect(debugConfig).to.have.property('showReturnValue', false); - expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).to.be.deep.equal([DebugOptions.RedirectOutput]); - }); - test('Test overriding defaults of debugger', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { redirectOutput: false } as LaunchRequestArguments); - - expect(debugConfig).to.have.property('console', 'integratedTerminal'); - expect(debugConfig).to.have.property('stopOnEntry', false); - expect(debugConfig).to.have.property('showReturnValue', false); - expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).to.be.deep.equal([]); - }); - async function testFixFilePathCase(isWindows: boolean, isMac: boolean, isLinux: boolean) { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath, isWindows, isMac, isLinux); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); - if (isWindows) { - expect(debugConfig).to.have.property('debugOptions').contains(DebugOptions.FixFilePathCase); - } else { - expect(debugConfig).to.have.property('debugOptions').not.contains(DebugOptions.FixFilePathCase); + + function setupIoc(pythonPath: string, workspaceFolder?: WorkspaceFolder) { + environmentActivationService = TypeMoq.Mock.ofType(); + environmentActivationService + .setup((e) => e.getActivatedEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(envVars)); + configService = TypeMoq.Mock.ofType(); + diagnosticsService = TypeMoq.Mock.ofType(); + debugEnvHelper = TypeMoq.Mock.ofType(); + pythonExecutionService = TypeMoq.Mock.ofType(); + helper = TypeMoq.Mock.ofType(); + const factory = TypeMoq.Mock.ofType(); + factory + .setup((f) => f.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonExecutionService.object)); + helper.setup((h) => h.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); + diagnosticsService + .setup((h) => h.validatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)); + + const settings = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + // 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); + debugEnvHelper + .setup((x) => x.getEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({})); + + debugProvider = new LaunchConfigurationResolver( + diagnosticsService.object, + configService.object, + debugEnvHelper.object, + interpreterService.object, + environmentActivationService.object, + ); } - } - test('Test fixFilePathCase for Windows', async () => { - await testFixFilePathCase(true, false, false); - }); - test('Test fixFilePathCase for Linux', async () => { - await testFixFilePathCase(false, false, true); - }); - test('Test fixFilePathCase for Mac', async () => { - await testFixFilePathCase(false, true, false); - }); - async function testPyramidConfiguration(isWindows: boolean, isLinux: boolean, isMac: boolean, addPyramidDebugOption: boolean = true, pyramidExists = true, shouldWork = true) { - const workspacePath = path.join('usr', 'development', 'wksp1'); - const pythonPath = path.join(workspacePath, 'env', 'bin', 'python'); - const pyramidFilePath = path.join(path.dirname(pythonPath), 'lib', 'site_packages', 'pyramid', '__init__.py'); - const pserveFilePath = path.join(path.dirname(pyramidFilePath), 'scripts', 'pserve.py'); - const args = ['-c', 'import pyramid;print(pyramid.__file__)']; - const workspaceFolder = createMoqWorkspaceFolder(workspacePath); - const pythonFile = 'xyz.py'; - - setupIoc(pythonPath, isWindows, isMac, isLinux); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - if (pyramidExists) { - pythonExecutionService.setup(e => e.exec(TypeMoq.It.isValue(args), TypeMoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: pyramidFilePath })) - .verifiable(TypeMoq.Times.exactly(addPyramidDebugOption ? 1 : 0)); - } else { - pythonExecutionService.setup(e => e.exec(TypeMoq.It.isValue(args), TypeMoq.It.isAny())) - .returns(() => Promise.reject('No Module Available')) - .verifiable(TypeMoq.Times.exactly(addPyramidDebugOption ? 1 : 0)); + + function setupActiveEditor(fileName: string | undefined, languageId: string) { + if (fileName) { + const textEditor = TypeMoq.Mock.ofType(); + const document = TypeMoq.Mock.ofType(); + document.setup((d) => d.languageId).returns(() => languageId); + document.setup((d) => d.fileName).returns(() => fileName); + textEditor.setup((t) => t.document).returns(() => document.object); + getActiveTextEditorStub.returns(textEditor.object); + } else { + getActiveTextEditorStub.returns(undefined); + } } - fileSystem.setup(f => f.fileExists(TypeMoq.It.isValue(pserveFilePath))) - .returns(() => Promise.resolve(pyramidExists)) - .verifiable(TypeMoq.Times.exactly(pyramidExists && addPyramidDebugOption ? 1 : 0)); - appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.exactly(pyramidExists || !addPyramidDebugOption ? 0 : 1)); - const options = addPyramidDebugOption ? { debugOptions: [DebugOptions.Pyramid], pyramid: true } : {}; - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, options as any as DebugConfiguration); - if (shouldWork) { - expect(debugConfig).to.have.property('program', pserveFilePath); - expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).contains(DebugOptions.Jinja); - } else { - expect(debugConfig!.program).to.be.not.equal(pserveFilePath); + function setupWorkspaces(folders: string[]) { + const workspaceFolders = folders.map(createMoqWorkspaceFolder); + getWorkspaceFolderStub.returns(workspaceFolders); } - pythonExecutionService.verifyAll(); - fileSystem.verifyAll(); - appShell.verifyAll(); - logger.verifyAll(); - } - test('Program is set for Pyramid (windows)', async () => { - await testPyramidConfiguration(true, false, false); - }); - test('Program is set for Pyramid (Linux)', async () => { - await testPyramidConfiguration(false, true, false); - }); - test('Program is set for Pyramid (Mac)', async () => { - await testPyramidConfiguration(false, false, true); - }); - test('Program is not set for Pyramid when DebugOption is not set (windows)', async () => { - await testPyramidConfiguration(true, false, false, false, false, false); - }); - test('Program is not set for Pyramid when DebugOption is not set (Linux)', async () => { - await testPyramidConfiguration(false, true, false, false, false, false); - }); - test('Program is not set for Pyramid when DebugOption is not set (Mac)', async () => { - await testPyramidConfiguration(false, false, true, false, false, false); - }); - test('Message is displayed when pyramid script does not exist (windows)', async () => { - await testPyramidConfiguration(true, false, false, true, false, false); - }); - test('Message is displayed when pyramid script does not exist (Linux)', async () => { - await testPyramidConfiguration(false, true, false, true, false, false); - }); - test('Message is displayed when pyramid script does not exist (Mac)', async () => { - await testPyramidConfiguration(false, false, true, true, false, false); - }); - test('Auto detect flask debugging', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { module: 'flask' } as any as DebugConfiguration); - - expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).contains(DebugOptions.RedirectOutput); - expect((debugConfig as any).debugOptions).contains(DebugOptions.Jinja); - }); - test('Test validation of Python Path when launching debugger (with invalid python path)', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - diagnosticsService.reset(); - diagnosticsService - .setup(h => h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.once()); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { redirectOutput: false, pythonPath } as LaunchRequestArguments); - - diagnosticsService.verifyAll(); - expect(debugConfig).to.be.equal(undefined, 'Not undefined'); - }); - test('Test validation of Python Path when launching debugger (with valid python path)', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - diagnosticsService.reset(); - diagnosticsService - .setup(h => h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { redirectOutput: false, pythonPath } as LaunchRequestArguments); - - diagnosticsService.verifyAll(); - expect(debugConfig).to.not.be.equal(undefined, 'is undefined'); - }); - async function testSetting(requestType: 'launch' | 'attach', settings: { [key: string]: boolean }, debugOptionName: DebugOptions, mustHaveDebugOption: boolean) { - setupIoc('pythonPath'); - const debugConfiguration: DebugConfiguration = { request: requestType, type: 'python', name: '', ...settings }; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, debugConfiguration); - if (mustHaveDebugOption) { - expect((debugConfig as any).debugOptions).contains(debugOptionName); - } else { - expect((debugConfig as any).debugOptions).not.contains(debugOptionName); + + const launch: LaunchRequestArguments = { + name: 'Python launch', + type: 'python', + request: 'launch', + }; + + async function resolveDebugConfiguration( + workspaceFolder: WorkspaceFolder | undefined, + launchConfig: Partial, + ) { + 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; } - } - type LaunchOrAttach = 'launch' | 'attach'; - const items: LaunchOrAttach[] = ['launch', 'attach']; - items.forEach(requestType => { - test(`Must not contain Sub Process when not specified (${requestType})`, async () => { - await testSetting(requestType, {}, DebugOptions.SubProcess, false); + + test('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 resolveDebugConfiguration(workspaceFolder, {}); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + 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'); + + 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); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + noDebug: true, + }); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + 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'); + + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); }); - test(`Must not contain Sub Process setting=false (${requestType})`, async () => { - await testSetting(requestType, { subProcess: false }, DebugOptions.SubProcess, false); + + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, createMoqWorkspaceFolder(path.dirname(pythonFile))); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + setupWorkspaces([]); + + const debugConfig = await resolveDebugConfiguration(undefined, {}); + const filePath = Uri.file(path.dirname('')).fsPath; + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + 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'); + + 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 resolveDebugConfiguration(undefined, {}); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + 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'); + + 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'; + setupIoc(pythonPath); + setupActiveEditor(activeFile, 'javascript'); + setupWorkspaces([]); + + const debugConfig = await resolveDebugConfiguration(undefined, {}); + + expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); + 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'); + + 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'; + const defaultWorkspace = path.join('usr', 'desktop'); + setupIoc(pythonPath, createMoqWorkspaceFolder(defaultWorkspace)); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + setupWorkspaces([defaultWorkspace]); + + 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('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'); + + expect(Object.keys((debugConfig as DebugConfiguration).env)).to.have.lengthOf(0); }); - test(`Must not contain Sub Process setting=true (${requestType})`, async () => { - await testSetting(requestType, { subProcess: true }, DebugOptions.SubProcess, true); + + test("Ensure 'port' is left unaltered", async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const port = 12341234; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + port, + }); + + expect(debugConfig).to.have.property('port', port); + }); + + test("Ensure 'localRoot' is left unaltered", async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + localRoot, + }); + + expect(debugConfig).to.have.property('localRoot', localRoot); + }); + + test("Ensure 'remoteRoot' is left unaltered", async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const remoteRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + remoteRoot, + }); + + expect(debugConfig).to.have.property('remoteRoot', remoteRoot); + }); + + test("Ensure 'localRoot' and 'remoteRoot' are not used", async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_Local_Root_${new Date().toString()}`; + const remoteRoot = `Debug_PythonPath_Remote_Root_${new Date().toString()}`; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + localRoot, + remoteRoot, + }); + + expect(debugConfig!.pathMappings).to.be.equal(undefined, 'unexpected pathMappings'); + }); + + test('Ensure non-empty path mappings are used', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const expected = { + localRoot: `Debug_PythonPath_Local_Root_${new Date().toString()}`, + remoteRoot: `Debug_PythonPath_Remote_Root_${new Date().toString()}`, + }; + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + pathMappings: [expected], + }); + + 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 resolveDebugConfiguration(workspaceFolder, { + ...launch, + pathMappings: [ + { + localRoot: '${workspaceFolder}/spam', + remoteRoot: '${workspaceFolder}/spam', + }, + ], + }); + + const { pathMappings } = debugConfig as LaunchRequestArguments; + expect(pathMappings).to.be.deep.equal([ + { + localRoot: `${workspaceFolder.uri.fsPath}/spam`, + remoteRoot: '${workspaceFolder}/spam', + }, + ]); + }); + + test('Ensure path mappings are not automatically added if missing', async () => { + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + localRoot, + }); + + 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); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + localRoot, + 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); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + localRoot, + pathMappings: [ + { + localRoot: '/spam', + remoteRoot: '.', + }, + ], + }); + + expect(debugConfig).to.have.property('localRoot', localRoot); + const { pathMappings } = debugConfig as LaunchRequestArguments; + expect(pathMappings).to.be.deep.equal([ + { + localRoot: '/spam', + remoteRoot: '.', + }, + ]); + }); + + test('Ensure drive letter is lower cased for local path mappings on Windows when with existing path mappings', async function () { + if (platform.getOSType() !== platform.OSType.Windows || osType !== platform.OSType.Windows) { + return this.skip(); + } + const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = Uri.file(path.join(workspaceFolder.uri.fsPath, 'app')).fsPath; + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + pathMappings: [ + { + localRoot, + remoteRoot: '/app/', + }, + ], + }); + + const { pathMappings } = debugConfig as LaunchRequestArguments; + const expected = Uri.file(`c${localRoot.substring(1)}`).fsPath; + expect(pathMappings).to.deep.equal([ + { + localRoot: expected, + 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 (platform.getOSType() === platform.OSType.Windows || osType === platform.OSType.Windows) { + return this.skip(); + } + const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = Uri.file(path.join(workspaceFolder.uri.fsPath, 'app')).fsPath; + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + pathMappings: [ + { + localRoot, + remoteRoot: '/app/', + }, + ], + }); + + const { pathMappings } = debugConfig as LaunchRequestArguments; + expect(pathMappings).to.deep.equal([ + { + localRoot, + 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 resolveDebugConfiguration(workspaceFolder, { + ...launch, + pathMappings: [ + { + localRoot: '/spam', + remoteRoot: '.', + }, + ], + }); + + const { pathMappings } = debugConfig as LaunchRequestArguments; + expect(pathMappings).to.deep.equal([ + { + localRoot: '/spam', + remoteRoot: '.', + }, + ]); + }); + + test('Ensure `${command:python.interpreterPath}` is replaced with actual pythonPath', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + setupIoc(pythonPath); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const debugConfig = await 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.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'; + 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, + 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.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); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + 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 === platform.OSType.Windows) { + expectedOptions.push(DebugOptions.FixFilePathCase); + } + expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal(expectedOptions); + }); + + test('Test defaults of python debugger', async () => { + if (DebuggerTypeName === 'python') { + return; + } + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + 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 DebugConfiguration).debugOptions).to.be.deep.equal([]); + }); + + test('Test overriding defaults of debugger', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + redirectOutput: true, + 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.ShowReturnValue, DebugOptions.RedirectOutput]; + if (osType === platform.OSType.Windows) { + expectedOptions.push(DebugOptions.FixFilePathCase); + } + expect((debugConfig as DebugConfiguration).debugOptions).to.be.deep.equal(expectedOptions); + }); + + const testsForRedirectOutput = [ + { + console: 'internalConsole', + redirectOutput: undefined, + expectedRedirectOutput: true, + }, + { + console: 'integratedTerminal', + redirectOutput: undefined, + expectedRedirectOutput: undefined, + }, + { + console: 'externalTerminal', + redirectOutput: undefined, + expectedRedirectOutput: undefined, + }, + { + console: 'internalConsole', + redirectOutput: false, + expectedRedirectOutput: false, + }, + { + console: 'integratedTerminal', + redirectOutput: false, + expectedRedirectOutput: false, + }, + { + console: 'externalTerminal', + redirectOutput: false, + expectedRedirectOutput: false, + }, + { + console: 'internalConsole', + redirectOutput: true, + expectedRedirectOutput: true, + }, + { + console: 'integratedTerminal', + redirectOutput: true, + expectedRedirectOutput: true, + }, + { + console: 'externalTerminal', + redirectOutput: true, + expectedRedirectOutput: true, + }, + ]; + test('Ensure redirectOutput property is correctly derived from console type', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + testsForRedirectOutput.forEach(async (testParams) => { + const debugConfig = await 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 DebugConfiguration).debugOptions).to.contain(DebugOptions.RedirectOutput); + } + }); + }); + + test('Test fixFilePathCase', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await 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'); + const workspaceFolder = createMoqWorkspaceFolder(workspacePath); + const pythonFile = 'xyz.py'; + + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + debugOptions: [DebugOptions.Pyramid], + pyramid: true, + }); + + expect(debugConfig).to.have.property('debugOptions'); + expect((debugConfig as DebugConfiguration).debugOptions).contains(DebugOptions.Jinja); + }); + + test('Auto detect flask debugging', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + module: 'flask', + }); + + expect(debugConfig).to.have.property('debugOptions'); + 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 "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); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + diagnosticsService.reset(); + diagnosticsService + .setup((h) => + 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(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 "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); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + diagnosticsService.reset(); + diagnosticsService + .setup((h) => + 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.atLeastOnce()); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + redirectOutput: false, + 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 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()), + ) + .returns(() => Promise.resolve(true)); + + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + ...launch, + redirectOutput: false, + pythonPath, + envFile: path.join('${workspaceFolder}', 'wow.envFile'), + }); + + expect(debugConfig!.envFile).to.be.equal(expectedEnvFilePath); + }); + + async function testSetting( + requestType: 'launch' | 'attach', + settings: Record, + debugOptionName: DebugOptions, + mustHaveDebugOption: boolean, + ) { + setupIoc('pythonPath'); + let debugConfig: DebugConfiguration = { + request: requestType, + type: 'python', + name: '', + ...settings, + }; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + + debugConfig = (await debugProvider.resolveDebugConfiguration!(workspaceFolder, debugConfig))!; + debugConfig = (await debugProvider.resolveDebugConfigurationWithSubstitutedVariables!( + workspaceFolder, + debugConfig, + ))!; + + if (mustHaveDebugOption) { + expect(debugConfig.debugOptions).contains(debugOptionName); + } else { + 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 () => { + await testSetting(requestType, {}, DebugOptions.SubProcess, false); + }); + test(`Must not contain Sub Process setting = false(${requestType})`, async () => { + await testSetting(requestType, { subProcess: false }, DebugOptions.SubProcess, false); + }); + test(`Must not contain Sub Process setting = true(${requestType})`, async () => { + await testSetting(requestType, { subProcess: true }, DebugOptions.SubProcess, true); + }); }); }); }); diff --git a/src/test/debugger/extension/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; + let debugService: typemoq.IMock; + let disposables: typemoq.IMock; + let interpreterService: typemoq.IMock; + let debugCommands: IExtensionSingleActivationService; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; + + setup(() => { + commandManager = typemoq.Mock.ofType(); + commandManager + .setup((c) => c.executeCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve()); + debugService = typemoq.Mock.ofType(); + disposables = typemoq.Mock.ofType(); + interpreterService = typemoq.Mock.ofType(); + 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 = (_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 1645efa1b79b..b1053def2eba 100644 --- a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts +++ b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts @@ -3,39 +3,70 @@ 'use strict'; -// tslint:disable:no-any - -import { instance, mock, verify, when } from 'ts-mockito'; +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 { PTVSDEvents } from '../../../../client/debugger/extension/hooks/constants'; +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 () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + await handler.handleCustomEvent(undefined as any); + verify(attachService.attach(anything(), anything())).never(); + }); test('Do not attach to child process if event is invalid', async () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); const body: any = {}; - await handler.handleCustomEvent({ event: 'abc', body, session: {} as any }); - verify(attachService.attach(body)).never(); + 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 event is invalid', async () => { + 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 = {}; - await handler.handleCustomEvent({ event: PTVSDEvents.ChildProcessLaunched, body, session: {} as any }); - verify(attachService.attach(body)).once(); + const session: any = { configuration: { type: 'other-type' } }; + await handler.handleCustomEvent({ event: 'abc', body, session }); + verify(attachService.attach(body, session)).never(); }); - test('Exceptions are not bubbled up if data is invalid', async () => { + test('Do not attach to child process if ptvsd_attach event is invalid', async () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); - await handler.handleCustomEvent(undefined as any); + const body: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; + await handler.handleCustomEvent({ event: DebuggerEvents.PtvsdAttachToSubprocess, body, session }); + verify(attachService.attach(body, session)).never(); }); - test('Exceptions are not bubbled up if exceptions are thrown', async () => { + test('Do not attach to child process if debugpy_attach event is invalid', async () => { const attachService = mock(ChildProcessAttachService); const handler = new ChildProcessAttachEventHandler(instance(attachService)); const body: any = {}; - when(attachService.attach(body)).thenThrow(new Error('Kaboom')); - await handler.handleCustomEvent({ event: PTVSDEvents.ChildProcessLaunched, body, session: {} as any }); - verify(attachService.attach(body)).once(); + const session: any = { configuration: { type: DebuggerTypeName } }; + await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); + verify(attachService.attach(body, session)).never(); + }); + test('Exceptions are not bubbled up if exceptions are thrown', async () => { + const attachService = mock(ChildProcessAttachService); + const handler = new ChildProcessAttachEventHandler(instance(attachService)); + const body: AttachRequestArguments = { + name: 'Attach', + type: 'python', + request: 'attach', + port: 1234, + subProcessId: 2, + }; + const session: any = { + configuration: { type: DebuggerTypeName }, + }; + when(attachService.attach(body, session)).thenThrow(new Error('Kaboom')); + 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 d04af425eb1c..118efe416e94 100644 --- a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts +++ b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts @@ -3,240 +3,193 @@ '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 { 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 { ChildProcessLaunchData } from '../../../../client/debugger/extension/hooks/types'; import { AttachRequestArguments, LaunchRequestArguments } from '../../../../client/debugger/types'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; suite('Debug - Attach to Child Process', () => { + let debugService: IDebugService; + let attachService: ChildProcessAttachService; + let getWorkspaceFoldersStub: sinon.SinonStub; + let showErrorMessageStub: sinon.SinonStub; + + setup(() => { + debugService = mock(DebugService); + 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 () => { - const shell = mock(ApplicationShell); - const debugService = mock(DebugService); - const workspaceService = mock(WorkspaceService); - const service = new ChildProcessAttachService(instance(shell), instance(debugService), instance(workspaceService)); - const args: LaunchRequestArguments | AttachRequestArguments = { - request: 'launch', + const data: AttachRequestArguments = { + name: 'Attach', type: 'python', - name: '' - }; - const data: ChildProcessLaunchData = { - rootProcessId: 1, - parentProcessId: 1, + request: 'attach', port: 1234, - processId: 2, - rootStartRequest: { - seq: 1, - type: 'python', - arguments: args, - command: 'request' - } + subProcessId: 2, }; + const session: any = {}; + getWorkspaceFoldersStub.returns(undefined); + when(debugService.startDebugging(anything(), anything(), anything())).thenResolve(true as any); + showErrorMessageStub.returns(undefined); - when(workspaceService.hasWorkspaceFolders).thenReturn(false); - when(debugService.startDebugging(anything(), anything())).thenResolve(true as any); - await service.attach(data); - verify(workspaceService.hasWorkspaceFolders).once(); - verify(debugService.startDebugging(anything(), anything())).once(); + await attachService.attach(data, session); + + sinon.assert.calledOnce(getWorkspaceFoldersStub); + verify(debugService.startDebugging(anything(), anything(), anything())).once(); + sinon.assert.notCalled(showErrorMessageStub); }); test('Message is displayed if debugger is not launched', async () => { - const shell = mock(ApplicationShell); - const debugService = mock(DebugService); - const workspaceService = mock(WorkspaceService); - const service = new ChildProcessAttachService(instance(shell), instance(debugService), instance(workspaceService)); - const args: LaunchRequestArguments | AttachRequestArguments = { - request: 'launch', + const data: AttachRequestArguments = { + name: 'Attach', type: 'python', - name: '' - }; - const data: ChildProcessLaunchData = { - rootProcessId: 1, - parentProcessId: 1, + request: 'attach', port: 1234, - processId: 2, - rootStartRequest: { - seq: 1, - type: 'python', - arguments: args, - command: 'request' - } + subProcessId: 2, }; - when(workspaceService.hasWorkspaceFolders).thenReturn(false); - when(debugService.startDebugging(anything(), anything())).thenResolve(false as any); - when(shell.showErrorMessage(anything())).thenResolve(); + const session: any = {}; + getWorkspaceFoldersStub.returns(undefined); + when(debugService.startDebugging(anything(), anything(), anything())).thenResolve(false as any); + showErrorMessageStub.resolves(() => {}); - await service.attach(data); + await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); - verify(debugService.startDebugging(anything(), anything())).once(); - verify(shell.showErrorMessage(anything())).once(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); + verify(debugService.startDebugging(anything(), anything(), anything())).once(); + sinon.assert.calledOnce(showErrorMessageStub); }); test('Use correct workspace folder', async () => { - const shell = mock(ApplicationShell); - const debugService = mock(DebugService); - const workspaceService = mock(WorkspaceService); - const service = new ChildProcessAttachService(instance(shell), instance(debugService), instance(workspaceService)); const rightWorkspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file('a') }; const wkspace1: WorkspaceFolder = { name: '0', index: 0, uri: Uri.file('0') }; const wkspace2: WorkspaceFolder = { name: '2', index: 2, uri: Uri.file('2') }; - const args: LaunchRequestArguments | AttachRequestArguments = { - request: 'launch', + const data: AttachRequestArguments = { + name: 'Attach', type: 'python', - name: '', - workspaceFolder: rightWorkspaceFolder.uri.fsPath - }; - const data: ChildProcessLaunchData = { - rootProcessId: 1, - parentProcessId: 1, + request: 'attach', port: 1234, - processId: 2, - rootStartRequest: { - seq: 1, - type: 'python', - arguments: args, - command: 'request' - } + subProcessId: 2, + workspaceFolder: rightWorkspaceFolder.uri.fsPath, }; - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - when(workspaceService.workspaceFolders).thenReturn([wkspace1, rightWorkspaceFolder, wkspace2]); - when(debugService.startDebugging(rightWorkspaceFolder, anything())).thenResolve(true as any); + const session: any = {}; + getWorkspaceFoldersStub.returns([wkspace1, rightWorkspaceFolder, wkspace2]); + when(debugService.startDebugging(rightWorkspaceFolder, anything(), anything())).thenResolve(true as any); - await service.attach(data); + await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); - verify(debugService.startDebugging(rightWorkspaceFolder, anything())).once(); - verify(shell.showErrorMessage(anything())).never(); + sinon.assert.called(getWorkspaceFoldersStub); + verify(debugService.startDebugging(rightWorkspaceFolder, anything(), anything())).once(); + sinon.assert.notCalled(showErrorMessageStub); }); test('Use empty workspace folder if right one is not found', async () => { - const shell = mock(ApplicationShell); - const debugService = mock(DebugService); - const workspaceService = mock(WorkspaceService); - const service = new ChildProcessAttachService(instance(shell), instance(debugService), instance(workspaceService)); const rightWorkspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file('a') }; const wkspace1: WorkspaceFolder = { name: '0', index: 0, uri: Uri.file('0') }; const wkspace2: WorkspaceFolder = { name: '2', index: 2, uri: Uri.file('2') }; - const args: LaunchRequestArguments | AttachRequestArguments = { - request: 'launch', + const data: AttachRequestArguments = { + name: 'Attach', type: 'python', - name: '', - workspaceFolder: rightWorkspaceFolder.uri.fsPath - }; - const data: ChildProcessLaunchData = { - rootProcessId: 1, - parentProcessId: 1, + request: 'attach', port: 1234, - processId: 2, - rootStartRequest: { - seq: 1, - type: 'python', - arguments: args, - command: 'request' - } + subProcessId: 2, + workspaceFolder: rightWorkspaceFolder.uri.fsPath, }; - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - when(workspaceService.workspaceFolders).thenReturn([wkspace1, wkspace2]); - when(debugService.startDebugging(undefined, anything())).thenResolve(true as any); + const session: any = {}; + getWorkspaceFoldersStub.returns([wkspace1, wkspace2]); + when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); - await service.attach(data); + await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); - verify(debugService.startDebugging(undefined, anything())).once(); - verify(shell.showErrorMessage(anything())).never(); + sinon.assert.called(getWorkspaceFoldersStub); + verify(debugService.startDebugging(undefined, anything(), anything())).once(); + sinon.assert.notCalled(showErrorMessageStub); }); - test('Validate debug config when parent/root parent was launched', async () => { - const shell = mock(ApplicationShell); - const debugService = mock(DebugService); - const workspaceService = mock(WorkspaceService); - const service = new ChildProcessAttachService(instance(shell), instance(debugService), instance(workspaceService)); - - const args: LaunchRequestArguments | AttachRequestArguments = { - request: 'launch', + test('Validate debug config is passed with the correct params', async () => { + const data: LaunchRequestArguments | AttachRequestArguments = { + request: 'attach', type: 'python', - name: '' - }; - const data: ChildProcessLaunchData = { - rootProcessId: 1, - parentProcessId: 1, + name: 'Attach', port: 1234, - processId: 2, - rootStartRequest: { - seq: 1, - type: 'python', - arguments: args, - command: 'request' - } + subProcessId: 2, + host: 'localhost', }; - const debugConfig = JSON.parse(JSON.stringify(args)); + const debugConfig = JSON.parse(JSON.stringify(data)); debugConfig.host = 'localhost'; - debugConfig.port = data.port; - debugConfig.name = `Child Process ${data.processId}`; - debugConfig.request = 'attach'; + const session: any = {}; - when(workspaceService.hasWorkspaceFolders).thenReturn(false); - when(debugService.startDebugging(undefined, anything())).thenResolve(true as any); - // when(debugService.startDebugging(undefined, debugConfig)).thenResolve(true as any); + getWorkspaceFoldersStub.returns(undefined); + when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); - await service.attach(data); + await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); - verify(debugService.startDebugging(undefined, anything())).once(); - const [, secondArg] = capture(debugService.startDebugging).last(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); + verify(debugService.startDebugging(undefined, anything(), anything())).once(); + const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); - 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 shell = mock(ApplicationShell); - const debugService = mock(DebugService); - const workspaceService = mock(WorkspaceService); - const service = new ChildProcessAttachService(instance(shell), instance(debugService), instance(workspaceService)); - - const args: AttachRequestArguments = { - request: 'attach', + test('Pass data as is if data is attach debug configuration', async () => { + const data: AttachRequestArguments = { type: 'python', + request: 'attach', name: '', - host: '123.123.123.123' }; - const data: ChildProcessLaunchData = { - rootProcessId: 1, - parentProcessId: 1, + const session: any = {}; + const debugConfig = JSON.parse(JSON.stringify(data)); + + getWorkspaceFoldersStub.returns(undefined); + when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); + + await attachService.attach(data, session); + + 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({ parentSession: session, lifecycleManagedByParent: true }); + sinon.assert.notCalled(showErrorMessageStub); + }); + test('Validate debug config when parent/root parent was attached', async () => { + const data: AttachRequestArguments = { + request: 'attach', + type: 'python', + name: 'Attach', + host: '123.123.123.123', port: 1234, - processId: 2, - rootStartRequest: { - seq: 1, - type: 'python', - arguments: args, - command: 'request' - } + subProcessId: 2, }; - const debugConfig = JSON.parse(JSON.stringify(args)); - debugConfig.host = args.host!; + const debugConfig = JSON.parse(JSON.stringify(data)); + debugConfig.host = data.host; debugConfig.port = data.port; - debugConfig.name = `Child Process ${data.processId}`; debugConfig.request = 'attach'; + const session: any = {}; - when(workspaceService.hasWorkspaceFolders).thenReturn(false); - when(debugService.startDebugging(undefined, anything())).thenResolve(true as any); - // when(debugService.startDebugging(undefined, debugConfig)).thenResolve(true as any); + getWorkspaceFoldersStub.returns(undefined); + when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); - await service.attach(data); + await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); - verify(debugService.startDebugging(undefined, anything())).once(); - const [, secondArg] = capture(debugService.startDebugging).last(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); + verify(debugService.startDebugging(undefined, anything(), anything())).once(); + const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); - 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 b2e6f0e9d8ce..056d722c7e0e 100644 --- a/src/test/debugger/extension/serviceRegistry.unit.test.ts +++ b/src/test/debugger/extension/serviceRegistry.unit.test.ts @@ -3,64 +3,112 @@ 'use strict'; -// tslint:disable:no-unnecessary-override no-invalid-template-strings max-func-body-length no-any - -import { expect } from 'chai'; -import * as typemoq from 'typemoq'; -import { DebuggerBanner } from '../../../client/debugger/extension/banner'; -import { ConfigurationProviderUtils } from '../../../client/debugger/extension/configuration/configurationProviderUtils'; -import { PythonDebugConfigurationService } from '../../../client/debugger/extension/configuration/debugConfigurationService'; -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 { 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 { instance, mock, verify } from 'ts-mockito'; +import { IExtensionSingleActivationService } from '../../../client/activation/types'; +import { DebugAdapterActivator } from '../../../client/debugger/extension/adapter/activator'; +import { DebugAdapterDescriptorFactory } from '../../../client/debugger/extension/adapter/factory'; +import { DebugSessionLoggingFactory } from '../../../client/debugger/extension/adapter/logging'; +import { OutdatedDebuggerPromptFactory } from '../../../client/debugger/extension/adapter/outdatedDebuggerPrompt'; +import { AttachProcessProviderFactory } from '../../../client/debugger/extension/attachQuickPick/factory'; +import { IAttachProcessProviderFactory } from '../../../client/debugger/extension/attachQuickPick/types'; import { AttachConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/attach'; import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; -import { IConfigurationProviderUtils, 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, IDebugConfigurationProvider, IDebugConfigurationService, IDebuggerBanner } from '../../../client/debugger/extension/types'; +import { + IDebugAdapterDescriptorFactory, + IDebugSessionLoggingFactory, + IOutdatedDebuggerPromptFactory, +} from '../../../client/debugger/extension/types'; +import { AttachRequestArguments, LaunchRequestArguments } from '../../../client/debugger/types'; +import { ServiceManager } from '../../../client/ioc/serviceManager'; import { IServiceManager } from '../../../client/ioc/types'; suite('Debugging - Service Registry', () => { + let serviceManager: IServiceManager; + setup(() => { + serviceManager = mock(ServiceManager); + }); test('Registrations', () => { - const serviceManager = typemoq.Mock.ofType(); - - [ - [IDebugConfigurationService, PythonDebugConfigurationService], - [IConfigurationProviderUtils, ConfigurationProviderUtils], - [IDebuggerBanner, DebuggerBanner], - [IChildProcessAttachService, ChildProcessAttachService], - [IDebugSessionEventHandlers, ChildProcessAttachEventHandler], - [IDebugConfigurationResolver, LaunchConfigurationResolver, 'launch'], - [IDebugConfigurationResolver, AttachConfigurationResolver, 'attach'], - [IDebugConfigurationProviderFactory, DebugConfigurationProviderFactory], - [IDebugConfigurationProvider, FileLaunchDebugConfigurationProvider, DebugConfigurationType.launchFile], - [IDebugConfigurationProvider, DjangoLaunchDebugConfigurationProvider, DebugConfigurationType.launchDjango], - [IDebugConfigurationProvider, FlaskLaunchDebugConfigurationProvider, DebugConfigurationType.launchFlask], - [IDebugConfigurationProvider, RemoteAttachDebugConfigurationProvider, DebugConfigurationType.remoteAttach], - [IDebugConfigurationProvider, ModuleLaunchDebugConfigurationProvider, DebugConfigurationType.launchModule], - [IDebugConfigurationProvider, PyramidLaunchDebugConfigurationProvider, DebugConfigurationType.launchPyramid] - ].forEach(mapping => { - if (mapping.length === 2) { - serviceManager - .setup(s => s.addSingleton(typemoq.It.isValue(mapping[0] as any), typemoq.It.isAny())) - .callback((_, cls) => expect(cls).to.equal(mapping[1])) - .verifiable(typemoq.Times.once()); - } else { - serviceManager - .setup(s => s.addSingleton(typemoq.It.isValue(mapping[0] as any), typemoq.It.isAny(), typemoq.It.isValue(mapping[2] as any))) - .callback((_, cls) => expect(cls).to.equal(mapping[1])) - .verifiable(typemoq.Times.once()); - } - }); + registerTypes(instance(serviceManager)); - registerTypes(serviceManager.object); - serviceManager.verifyAll(); + verify( + serviceManager.addSingleton( + IChildProcessAttachService, + ChildProcessAttachService, + ), + ).once(); + verify( + serviceManager.addSingleton( + IExtensionSingleActivationService, + DebugAdapterActivator, + ), + ).once(); + verify( + serviceManager.addSingleton( + IDebugAdapterDescriptorFactory, + DebugAdapterDescriptorFactory, + ), + ).once(); + verify( + serviceManager.addSingleton( + IDebugSessionEventHandlers, + ChildProcessAttachEventHandler, + ), + ).once(); + verify( + serviceManager.addSingleton>( + IDebugConfigurationResolver, + LaunchConfigurationResolver, + 'launch', + ), + ).once(); + verify( + serviceManager.addSingleton>( + IDebugConfigurationResolver, + AttachConfigurationResolver, + 'attach', + ), + ).once(); + verify( + serviceManager.addSingleton( + IExtensionSingleActivationService, + DebugAdapterActivator, + ), + ).once(); + verify( + serviceManager.addSingleton( + IDebugAdapterDescriptorFactory, + DebugAdapterDescriptorFactory, + ), + ).once(); + verify( + serviceManager.addSingleton( + IDebugSessionLoggingFactory, + DebugSessionLoggingFactory, + ), + ).once(); + verify( + serviceManager.addSingleton( + IOutdatedDebuggerPromptFactory, + OutdatedDebuggerPromptFactory, + ), + ).once(); + verify( + serviceManager.addSingleton( + IAttachProcessProviderFactory, + AttachProcessProviderFactory, + ), + ).once(); + verify( + serviceManager.addSingleton( + IExtensionSingleActivationService, + DebugCommands, + ), + ).once(); }); }); diff --git a/src/test/debugger/misc.test.ts b/src/test/debugger/misc.test.ts deleted file mode 100644 index fc341f37a8c6..000000000000 --- a/src/test/debugger/misc.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// tslint:disable:no-suspicious-comment max-func-body-length no-invalid-this no-var-requires no-require-imports no-any - -import * as path from 'path'; -import { DebugClient } from 'vscode-debugadapter-testsupport'; -import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import { IS_WINDOWS } from '../../client/common/platform/constants'; -import { noop } from '../../client/common/utils/misc'; -import { DebuggerTypeName, PTVSD_PATH } from '../../client/debugger/constants'; -import { DebugOptions, LaunchRequestArguments } from '../../client/debugger/types'; -import { PYTHON_PATH, sleep } from '../common'; -import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; -import { DEBUGGER_TIMEOUT } from './common/constants'; -import { DebugClientEx } from './debugClient'; - -const debugFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'debugging'); - -const EXPERIMENTAL_DEBUG_ADAPTER = path.join(__dirname, '..', '..', 'client', 'debugger', 'debugAdapter', 'main.js'); - -let testCounter = 0; -const testAdapterFilePath = EXPERIMENTAL_DEBUG_ADAPTER; -const debuggerType = DebuggerTypeName; -suite(`Standard Debugging - Misc tests: ${debuggerType}`, () => { - - let debugClient: DebugClient; - setup(async function () { - if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { - this.skip(); - } - await new Promise(resolve => setTimeout(resolve, 1000)); - debugClient = createDebugAdapter(); - debugClient.defaultTimeout = DEBUGGER_TIMEOUT; - await debugClient.start(); - }); - teardown(async () => { - // Wait for a second before starting another test (sometimes, sockets take a while to get closed). - await sleep(1000); - try { - await debugClient.stop().catch(noop); - // tslint:disable-next-line:no-empty - } catch (ex) { } - await sleep(1000); - }); - /** - * Creates the debug adapter. - * We do not need to support code coverage on AppVeyor, lets use the standard test adapter. - * @returns {DebugClient} - */ - function createDebugAdapter(): DebugClient { - if (IS_WINDOWS) { - return new DebugClient('node', testAdapterFilePath, debuggerType); - } else { - const coverageDirectory = path.join(EXTENSION_ROOT_DIR, `debug_coverage${testCounter += 1}`); - return new DebugClientEx(testAdapterFilePath, debuggerType, coverageDirectory, { cwd: EXTENSION_ROOT_DIR }); - } - } - function buildLaunchArgs(pythonFile: string, stopOnEntry: boolean = false, showReturnValue: boolean = false): LaunchRequestArguments { - const env = { PYTHONPATH: PTVSD_PATH }; - // tslint:disable-next-line:no-unnecessary-local-variable - const options = { - program: path.join(debugFilesPath, pythonFile), - cwd: debugFilesPath, - stopOnEntry, - showReturnValue, - debugOptions: [DebugOptions.RedirectOutput], - pythonPath: PYTHON_PATH, - args: [], - env, - envFile: '', - logToFile: false, - type: debuggerType - } as any as LaunchRequestArguments; - - return options; - } - - test('Should run program to the end', async () => { - await Promise.all([ - debugClient.configurationSequence(), - debugClient.launch(buildLaunchArgs('simplePrint.py', false)), - debugClient.waitForEvent('initialized'), - debugClient.waitForEvent('terminated') - ]); - }); - test('test stderr output for Python', async () => { - await Promise.all([ - debugClient.configurationSequence(), - debugClient.launch(buildLaunchArgs('stdErrOutput.py', false)), - debugClient.waitForEvent('initialized'), - //TODO: ptvsd does not differentiate. - debugClient.assertOutput('stderr', 'error output'), - debugClient.waitForEvent('terminated') - ]); - }); - test('Test stdout output', async () => { - await Promise.all([ - debugClient.configurationSequence(), - debugClient.launch(buildLaunchArgs('stdOutOutput.py', false)), - debugClient.waitForEvent('initialized'), - debugClient.assertOutput('stdout', 'normal output'), - debugClient.waitForEvent('terminated') - ]); - }); -}); diff --git a/src/test/debugger/portAndHost.test.ts b/src/test/debugger/portAndHost.test.ts deleted file mode 100644 index abb30c277f82..000000000000 --- a/src/test/debugger/portAndHost.test.ts +++ /dev/null @@ -1,119 +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 getFreePort from 'get-port'; -import * as net from 'net'; -import * as path from 'path'; -import { DebugClient } from 'vscode-debugadapter-testsupport'; -import { noop } from '../../client/common/utils/misc'; -import { DebuggerTypeName } from '../../client/debugger/constants'; -import { DebugOptions, LaunchRequestArguments } from '../../client/debugger/types'; -import { PYTHON_PATH } from '../common'; -import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; -import { DEBUGGER_TIMEOUT } from './common/constants'; - -use(chaiAsPromised); - -const debugFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'debugging'); - -const EXPERIMENTAL_DEBUG_ADAPTER = path.join(__dirname, '..', '..', 'client', 'debugger', 'debugAdapter', 'main.js'); - -const testAdapterFilePath = EXPERIMENTAL_DEBUG_ADAPTER; -const debuggerType = DebuggerTypeName; -// tslint:disable-next-line:max-func-body-length -suite(`Standard Debugging of ports and hosts: ${debuggerType}`, () => { - let debugClient: DebugClient; - setup(async function () { - if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - } - await new Promise(resolve => setTimeout(resolve, 1000)); - debugClient = new DebugClient('node', testAdapterFilePath, debuggerType); - debugClient.defaultTimeout = DEBUGGER_TIMEOUT; - await debugClient.start(); - }); - teardown(async () => { - // Wait for a second before starting another test (sometimes, sockets take a while to get closed). - await new Promise(resolve => setTimeout(resolve, 1000)); - try { - debugClient.stop().catch(noop); - // tslint:disable-next-line:no-empty - } catch (ex) { } - }); - - function buildLaunchArgs(pythonFile: string, stopOnEntry: boolean = false, port?: number, host?: string, showReturnValue: boolean = false): LaunchRequestArguments { - return { - program: path.join(debugFilesPath, pythonFile), - cwd: debugFilesPath, - stopOnEntry, - showReturnValue, - logToFile: true, - debugOptions: [DebugOptions.RedirectOutput], - pythonPath: PYTHON_PATH, - args: [], - envFile: '', - host, port, - type: debuggerType, - name: '', - request: 'launch' - }; - } - - async function testDebuggingWithProvidedPort(port?: number | undefined, host?: string | undefined) { - await Promise.all([ - debugClient.configurationSequence(), - debugClient.launch(buildLaunchArgs('startAndWait.py', false, port, host)), - debugClient.waitForEvent('initialized') - ]); - - // Confirm port is in use (if one was provided). - if (typeof port === 'number' && port > 0) { - // We know the port 'debuggerPort' was free, now that the debugger has started confirm that this port is no longer free. - const portBasedOnDebuggerPort = await getFreePort({ host: 'localhost', port }); - expect(portBasedOnDebuggerPort).is.not.equal(port, 'Port assigned to debugger not used by the debugger'); - } - } - - test('Confirm debuggig works if both port and host are not provided', async () => { - await testDebuggingWithProvidedPort(); - }); - - test('Confirm debuggig works if port=0', async () => { - await testDebuggingWithProvidedPort(0, 'localhost'); - }); - - test('Confirm debuggig works if port=0 or host=localhost', async () => { - await testDebuggingWithProvidedPort(0, 'localhost'); - }); - - test('Confirm debuggig works if port=0 or host=127.0.0.1', async () => { - await testDebuggingWithProvidedPort(0, '127.0.0.1'); - }); - - test('Confirm debuggig fails when an invalid host is provided', async () => { - const promise = testDebuggingWithProvidedPort(0, 'xyz123409924ple_ewf'); - let exception: Error | undefined; - try { - await promise; - } catch (ex) { - exception = ex; - } - expect(exception!.message).contains('ENOTFOUND', 'Debugging failed for some other reason'); - }); - test('Confirm debuggig fails when provided port is in use', async () => { - const server = net.createServer(noop); - const port = await new Promise((resolve, reject) => server.listen({ host: 'localhost', port: 0 }, () => resolve(server.address().port))); - let exception: Error | undefined; - try { - await testDebuggingWithProvidedPort(port); - } catch (ex) { - exception = ex; - } finally { - server.close(); - } - expect(exception!.message).contains('EADDRINUSE', 'Debugging failed for some other reason'); - }); -}); diff --git a/src/test/debugger/run.test.ts b/src/test/debugger/run.test.ts deleted file mode 100644 index c27eb78f4b67..000000000000 --- a/src/test/debugger/run.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-invalid-this no-require-imports no-require-imports no-var-requires - -import * as path from 'path'; -import { DebugClient } from 'vscode-debugadapter-testsupport'; -import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import { noop } from '../../client/common/utils/misc'; -import { DebuggerTypeName, PTVSD_PATH } from '../../client/debugger/constants'; -import { DebugOptions, LaunchRequestArguments } from '../../client/debugger/types'; -import { PYTHON_PATH, sleep } from '../common'; -import { IS_MULTI_ROOT_TEST, TEST_DEBUGGER } from '../initialize'; -import { createDebugAdapter } from './utils'; - -const debugFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'debugging'); -const debuggerType = DebuggerTypeName; -suite('Run without Debugging', () => { - let debugClient: DebugClient; - setup(async function () { - if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { - this.skip(); - } - await new Promise(resolve => setTimeout(resolve, 1000)); - const coverageDirectory = path.join(EXTENSION_ROOT_DIR, `debug_coverage_nodebug${this.currentTest.title}`); - debugClient = await createDebugAdapter(coverageDirectory); - }); - teardown(async () => { - // Wait for a second before starting another test (sometimes, sockets take a while to get closed). - await sleep(1000); - try { - await debugClient.stop().catch(noop); - // tslint:disable-next-line:no-empty - } catch (ex) { } - await sleep(1000); - }); - function buildLaunchArgs(pythonFile: string, stopOnEntry: boolean = false, showReturnValue: boolean = false): LaunchRequestArguments { - // tslint:disable-next-line:no-unnecessary-local-variable - return { - program: path.join(debugFilesPath, pythonFile), - cwd: debugFilesPath, - stopOnEntry, - showReturnValue, - noDebug: true, - debugOptions: [DebugOptions.RedirectOutput], - pythonPath: PYTHON_PATH, - args: [], - env: { PYTHONPATH: PTVSD_PATH }, - envFile: '', - logToFile: false, - type: debuggerType, - name: '', - request: 'launch' - }; - } - - test('Should run program to the end', async () => { - await Promise.all([ - debugClient.configurationSequence(), - debugClient.launch(buildLaunchArgs('simplePrint.py', false)), - debugClient.waitForEvent('initialized'), - debugClient.waitForEvent('terminated') - ]); - }); - test('test stderr output for Python', async () => { - await Promise.all([ - debugClient.configurationSequence(), - debugClient.launch(buildLaunchArgs('stdErrOutput.py', false)), - debugClient.waitForEvent('initialized'), - debugClient.assertOutput('stderr', 'error output'), - debugClient.waitForEvent('terminated') - ]); - }); - test('Test stdout output', async () => { - await Promise.all([ - debugClient.configurationSequence(), - debugClient.launch(buildLaunchArgs('stdOutOutput.py', false)), - debugClient.waitForEvent('initialized'), - debugClient.assertOutput('stdout', 'normal output'), - debugClient.waitForEvent('terminated') - ]); - }); -}); diff --git a/src/test/debugger/utils.ts b/src/test/debugger/utils.ts index 6fe27f09ee25..9ccb8958b660 100644 --- a/src/test/debugger/utils.ts +++ b/src/test/debugger/utils.ts @@ -3,94 +3,363 @@ 'use strict'; -// tslint:disable:no-any no-http-string - import { expect } from 'chai'; +import * as fs from '../../client/common/platform/fs-paths'; import * as path from 'path'; -import * as request from 'request'; -import { DebugClient } from 'vscode-debugadapter-testsupport'; -import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; +import * as vscode from 'vscode'; +import { DebugProtocol } from 'vscode-debugprotocol'; import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import { IS_WINDOWS } from '../../client/common/platform/constants'; -import { DebuggerTypeName } from '../../client/debugger/constants'; -import { DEBUGGER_TIMEOUT } from './common/constants'; -import { DebugClientEx } from './debugClient'; - -const testAdapterFilePath = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'debugAdapter', 'main.js'); -const debuggerType = DebuggerTypeName; - -/** - * Creates the debug adapter. - * We do not need to support code coverage on AppVeyor, lets use the standard test adapter. - * @returns {DebugClient} - */ -export async function createDebugAdapter(coverageDirectory: string): Promise { - await new Promise(resolve => setTimeout(resolve, 1000)); - let debugClient: DebugClient; - if (IS_WINDOWS) { - debugClient = new DebugClient('node', testAdapterFilePath, debuggerType); - } else { - debugClient = new DebugClientEx(testAdapterFilePath, debuggerType, coverageDirectory, { cwd: EXTENSION_ROOT_DIR }); - } - debugClient.defaultTimeout = DEBUGGER_TIMEOUT; - await debugClient.start(); - return debugClient; +import { sleep } from '../../client/common/utils/async'; +import { getDebugpyLauncherArgs } from '../../client/debugger/extension/adapter/remoteLaunchers'; +import { PythonFixture } from '../fixtures'; +import { Proc, ProcOutput, ProcResult } from '../proc'; + +const launchJSON = path.join(EXTENSION_ROOT_DIR, 'src', 'test', '.vscode', 'launch.json'); + +export function getConfig(name: string): vscode.DebugConfiguration { + const configs = fs.readJSONSync(launchJSON); + for (const config of configs.configurations) { + if (config.name === name) { + return config; + } + } + throw Error(`debug config "${name}" not found`); } -export async function continueDebugging(debugClient: DebugClient) { - const threads = await debugClient.threadsRequest(); - expect(threads).to.be.not.equal(undefined, 'no threads response'); - expect(threads.body.threads).to.be.lengthOf(1); +type DAPSource = 'vscode' | 'debugpy'; +type DAPHandler = (src: DAPSource, msg: DebugProtocol.ProtocolMessage) => void; + +type TrackedDebugger = { + id: number; + output: ProcOutput; + dapHandler?: DAPHandler; + session?: vscode.DebugSession; + exitCode?: number; +}; + +class DebugAdapterTracker { + constructor( + // This contains all the state. + private readonly tracked: TrackedDebugger, + ) {} + + // debugpy -> VS Code - await debugClient.continueRequest({ threadId: threads.body.threads[0].id }); + public onDidSendMessage(message: any): void { + this.onDAPMessage('debugpy', message as DebugProtocol.ProtocolMessage); + } + + // VS Code -> debugpy + + public onWillReceiveMessage(message: any): void { + this.onDAPMessage('vscode', message as DebugProtocol.ProtocolMessage); + } + + public onExit(code: number | undefined, signal: string | undefined): void { + if (code) { + this.tracked.exitCode = code; + } else if (signal) { + this.tracked.exitCode = 1; + } else { + this.tracked.exitCode = 0; + } + } + + // The following vscode.DebugAdapterTracker methods are not implemented: + // + // * onWillStartSession(): void; + // * onWillStopSession(): void; + // * onError(error: Error): void; + + private onDAPMessage(src: DAPSource, msg: DebugProtocol.ProtocolMessage) { + // Unomment this to see the DAP messages sent between VS Code and debugpy: + //console.log(`| DAP (${src === 'vscode' ? 'VS Code -> debugpy' : 'debugpy -> VS Code'})\n`, msg, '\n| DAP'); + + // See: https://microsoft.github.io/debug-adapter-protocol/specification + if (this.tracked.dapHandler) { + this.tracked.dapHandler(src, msg); + } + if (msg.type === 'event') { + const event = ((msg as unknown) as DebugProtocol.Event).event; + if (event === 'output') { + this.onOutputEvent((msg as unknown) as DebugProtocol.OutputEvent); + } + } + } + + private onOutputEvent(msg: DebugProtocol.OutputEvent) { + if (msg.body.category === undefined) { + msg.body.category = 'stdout'; + } + const data = Buffer.from(msg.body.output, 'utf-8'); + if (msg.body.category === 'stdout') { + this.tracked.output.addStdout(data); + } else if (msg.body.category === 'stderr') { + this.tracked.output.addStderr(data); + } else { + // Ignore it. + } + } } -export type ExpectedVariable = { type: string; name: string; value: string }; -export async function validateVariablesInFrame(debugClient: DebugClient, - stackTrace: DebugProtocol.StackTraceResponse, - expectedVariables: ExpectedVariable[], numberOfScopes?: number) { +class Debuggers { + private nextID = 0; + private tracked: { [id: number]: TrackedDebugger } = {}; + private results: { [id: number]: ProcResult } = {}; + + public track(config: vscode.DebugConfiguration, output?: ProcOutput): number { + if (this.nextID === 0) { + vscode.debug.registerDebugAdapterTrackerFactory('python', this); + } + if (output === undefined) { + output = new ProcOutput(); + } + this.nextID += 1; + const id = this.nextID; + this.tracked[id] = { id, output }; + config._test_session_id = id; + return id; + } - const frameId = stackTrace.body.stackFrames[0].id; + public setDAPHandler(id: number, handler: DAPHandler) { + const tracked = this.tracked[id]; + if (tracked !== undefined) { + tracked.dapHandler = handler; + } + } - const scopes = await debugClient.scopesRequest({ frameId }); - if (numberOfScopes) { - expect(scopes.body.scopes).of.length(1, 'Incorrect number of scopes'); + public getSession(id: number): vscode.DebugSession | undefined { + const tracked = this.tracked[id]; + if (tracked === undefined) { + return undefined; + } else { + return tracked.session; + } } - const variablesReference = scopes.body.scopes[0].variablesReference; - const variables = await debugClient.variablesRequest({ variablesReference }); + public async waitUntilDone(id: number): Promise { + const cachedResult = this.results[id]; + if (cachedResult !== undefined) { + return cachedResult; + } + const tracked = this.tracked[id]; + if (tracked === undefined) { + throw Error(`untracked debugger ${id}`); + } else { + while (tracked.exitCode === undefined) { + await sleep(10); // milliseconds + } + const result = { + exitCode: tracked.exitCode, + stdout: tracked.output.stdout, + }; + this.results[id] = result; + return result; + } + } - for (const expectedVariable of expectedVariables) { - const variable = variables.body.variables.find(item => item.name === expectedVariable.name)!; - expect(variable).to.be.not.equal('undefined', `variable '${expectedVariable.name}' is undefined`); - expect(variable.type).to.be.equal(expectedVariable.type); - expect(variable.value).to.be.equal(expectedVariable.value); + // This is for DebugAdapterTrackerFactory: + public createDebugAdapterTracker(session: vscode.DebugSession): vscode.ProviderResult { + const id = session.configuration._test_session_id; + const tracked = this.tracked[id]; + if (tracked !== undefined) { + tracked.session = session; + return new DebugAdapterTracker(tracked); + } else if (id !== undefined) { + // This should not have happened, but we don't worry about + // it for now. + } + return undefined; } } -export function makeHttpRequest(uri: string): Promise { - return new Promise((resolve, reject) => { - request.get(uri, (error: any, response: request.Response, body: any) => { - if (error) { - return reject(error); +const debuggers = new Debuggers(); + +class DebuggerSession { + private started: boolean = false; + private raw: vscode.DebugSession | undefined; + private stopped: { breakpoint: boolean; threadId: number } | undefined; + constructor( + public readonly id: number, + public readonly config: vscode.DebugConfiguration, + private readonly wsRoot?: vscode.WorkspaceFolder, + private readonly proc?: Proc, + ) {} + + public async start() { + if (this.started) { + throw Error('already started'); + } + this.started = true; + + // Un-comment this to see the debug config used in this session: + //console.log('|', session.config, '|'); + const started = await vscode.debug.startDebugging(this.wsRoot, this.config); + expect(started).to.be.equal(true, 'Debugger did not sart'); + this.raw = debuggers.getSession(this.id); + expect(this.raw).to.not.equal(undefined, 'session not set'); + } + + public async waitUntilDone(): Promise { + if (this.proc) { + return this.proc.waitUntilDone(); + } else { + return debuggers.waitUntilDone(this.id); + } + } + + public addBreakpoint(filename: string, line: number, ch?: number): vscode.Breakpoint { + // The arguments are 1-indexed. + const loc = new vscode.Location( + vscode.Uri.file(filename), + // VS Code wants 0-indexed line and column numbers. + // We default to the beginning of the line. + new vscode.Position(line - 1, ch ? ch - 1 : 0), + ); + const bp = new vscode.SourceBreakpoint(loc); + vscode.debug.addBreakpoints([bp]); + return bp; + } + + public async waitForBreakpoint(bp: vscode.Breakpoint, opts: { clear: boolean } = { clear: true }) { + while (!this.stopped || !this.stopped.breakpoint) { + await sleep(10); // milliseconds + } + if (opts.clear) { + vscode.debug.removeBreakpoints([bp]); + await this.raw!.customRequest('continue', { threadId: this.stopped.threadId }); + this.stopped = undefined; + } + } + + public handleDAPMessage(_src: DAPSource, baseMsg: DebugProtocol.ProtocolMessage) { + if (baseMsg.type === 'event') { + const event = ((baseMsg as unknown) as DebugProtocol.Event).event; + if (event === 'stopped') { + const msg = (baseMsg as unknown) as DebugProtocol.StoppedEvent; + this.stopped = { + breakpoint: msg.body.reason === 'breakpoint', + threadId: (msg.body.threadId as unknown) as number, + }; + } else { + // For now there aren't any other events we care about. } - if (response.statusCode !== 200) { - reject(new Error(`Status code = ${response.statusCode}`)); + } else if (baseMsg.type === 'request') { + // For now there aren't any requests we care about. + } else if (baseMsg.type === 'response') { + // For now there aren't any responses we care about. + } else { + // This shouldn't happen but for now we don't worry about it. + } + } + + // The old debug adapter tests used + // 'vscode-debugadapter-testsupport'.DebugClient to interact with + // the debugger. This is helpful info when we are considering + // additional debugger-related tests. Here are the methods/props + // the old tests used: + // + // * defaultTimeout + // * start() + // * stop() + // * initializeRequest() + // * configurationSequence() + // * launch() + // * attachRequest() + // * waitForEvent() + // * assertOutput() + // * threadsRequest() + // * continueRequest() + // * scopesRequest() + // * variablesRequest() + // * setBreakpointsRequest() + // * setExceptionBreakpointsRequest() + // * assertStoppedLocation() +} + +export class DebuggerFixture extends PythonFixture { + public async resolveDebugger( + configName: string, + file: string, + scriptArgs: string[], + wsRoot?: vscode.WorkspaceFolder, + ): Promise { + const config = getConfig(configName); + let proc: Proc | undefined; + if (config.request === 'launch') { + config.program = file; + config.args = scriptArgs; + config.redirectOutput = false; + // XXX set the file in the current vscode editor? + } else if (config.request === 'attach') { + if (config.port) { + proc = await this.runDebugger(config.port, file, ...scriptArgs); + if (wsRoot && config.name === 'attach to a local port') { + config.pathMappings.localRoot = wsRoot.uri.fsPath; + } + } else if (config.processId) { + proc = this.runScript(file, ...scriptArgs); + config.processId = proc.pid; } else { - resolve(body.toString()); + throw Error(`unsupported attach config "${configName}"`); } + if (wsRoot && config.pathMappings) { + config.pathMappings.localRoot = wsRoot.uri.fsPath; + } + } else { + throw Error(`unsupported request type "${config.request}"`); + } + const id = debuggers.track(config); + const session = new DebuggerSession(id, config, wsRoot, proc); + debuggers.setDAPHandler(id, (src, msg) => session.handleDAPMessage(src, msg)); + return session; + } + + public getLaunchTarget(filename: string, args: string[]): vscode.DebugConfiguration { + return { + type: 'python', + name: 'debug', + request: 'launch', + program: filename, + args: args, + console: 'integratedTerminal', + }; + } + + public getAttachTarget(filename: string, args: string[], port?: number): vscode.DebugConfiguration { + if (port) { + this.runDebugger(port, filename, ...args); + return { + type: 'python', + name: 'debug', + request: 'attach', + port: port, + host: 'localhost', + pathMappings: [ + { + localRoot: '${workspaceFolder}', + remoteRoot: '.', + }, + ], + }; + } else { + const proc = this.runScript(filename, ...args); + return { + type: 'python', + name: 'debug', + request: 'attach', + processId: proc.pid, + }; + } + } + + 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, }); - }); -} -export async function hitHttpBreakpoint(debugClient: DebugClient, uri: string, file: string, line: number): Promise<[DebugProtocol.StackTraceResponse, Promise]> { - const breakpointLocation = { path: file, column: 1, line }; - await debugClient.setBreakpointsRequest({ - lines: [breakpointLocation.line], - breakpoints: [{ line: breakpointLocation.line, column: breakpointLocation.column }], - source: { path: breakpointLocation.path } - }); - - // Make the request, we want the breakpoint to be hit. - const breakpointPromise = debugClient.assertStoppedLocation('breakpoint', breakpointLocation); - const httpResult = makeHttpRequest(uri); - return [await breakpointPromise, httpResult]; + 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 de610f5b05e2..949f14caee3d 100644 --- a/src/test/debuggerTest.ts +++ b/src/test/debuggerTest.ts @@ -1,17 +1,27 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// tslint:disable:no-console no-require-imports no-var-requires - import * as path from 'path'; +import { runTests } from '@vscode/test-electron'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; +import { getChannel } from './utils/vscode'; -process.env.CODE_TESTS_WORKSPACE = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); +const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); process.env.IS_CI_SERVER_TEST_DEBUGGER = '1'; process.env.VSC_PYTHON_CI_TEST = '1'; function start() { console.log('*'.repeat(100)); console.log('Start Debugger tests'); - require('../../node_modules/vscode/bin/test'); + runTests({ + extensionDevelopmentPath: EXTENSION_ROOT_DIR_FOR_TESTS, + extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), + launchArgs: [workspacePath], + version: getChannel(), + extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, + }).catch((ex) => { + console.error('End Debugger tests (with errors)', ex); + process.exit(1); + }); } start(); diff --git a/src/test/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; + let discoverAPI: typemoq.IMock; + let interpreterPathService: typemoq.IMock; + let configService: typemoq.IMock; + let extensions: typemoq.IMock; + let workspaceService: typemoq.IMock; + let envVarsProvider: typemoq.IMock; + let onDidChangeRefreshState: EventEmitter; + let onDidChangeEnvironments: EventEmitter; + let onDidChangeEnvironmentVariables: EventEmitter; + + let environmentApi: PythonExtension['environments']; + + setup(() => { + serviceContainer = typemoq.Mock.ofType(); + 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(); + extensions = typemoq.Mock.ofType(); + workspaceService = typemoq.Mock.ofType(); + envVarsProvider = typemoq.Mock.ofType(); + extensions + .setup((e) => e.determineExtensionFromCallStack()) + .returns(() => Promise.resolve({ extensionId: 'id', displayName: 'displayName', apiName: 'apiName' })); + interpreterPathService = typemoq.Mock.ofType(); + configService = typemoq.Mock.ofType(); + 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(); + 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(); + 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 new file mode 100644 index 000000000000..c55c88c04d3b --- /dev/null +++ b/src/test/extension-version.functional.test.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as glob from 'glob'; +import * as path from 'path'; +import { ApplicationEnvironment } from '../client/common/application/applicationEnvironment'; +import { IApplicationEnvironment } from '../client/common/application/types'; +import { EXTENSION_ROOT_DIR } from '../client/common/constants'; + +suite('Extension version tests', () => { + let version: string; + let applicationEnvironment: IApplicationEnvironment; + const branchName = process.env.CI_BRANCH_NAME; + + suiteSetup(async function () { + // Skip the entire suite if running locally + if (!branchName) { + return this.skip(); + } + }); + + setup(() => { + applicationEnvironment = new ApplicationEnvironment(undefined as any, undefined as any, undefined as any); + version = applicationEnvironment.packageJson.version; + }); + + test('If we are running a pipeline in the main branch, the extension version in `package.json` should have the "-dev" suffix', async function () { + if (branchName !== 'main') { + return this.skip(); + } + + return expect( + version.endsWith('-dev'), + 'When running a pipeline in the main branch, the extension version in package.json should have the -dev suffix', + ).to.be.true; + }); + + test('If we are running a pipeline in the release branch, the extension version in `package.json` should not have the "-dev" suffix', async function () { + if (!branchName!.startsWith('release')) { + return this.skip(); + } + + return expect( + version.endsWith('-dev'), + 'When running a pipeline in the release branch, the extension version in package.json should not have the -dev suffix', + ).to.be.false; + }); +}); + +suite('Extension localization files', () => { + test('Load localization file', () => { + const filesFailed: string[] = []; + glob.sync('package.nls.*.json', { sync: true, cwd: EXTENSION_ROOT_DIR }).forEach((localizationFile) => { + try { + JSON.parse(fs.readFileSync(path.join(EXTENSION_ROOT_DIR, localizationFile)).toString()); + } catch { + filesFailed.push(localizationFile); + } + }); + + expect(filesFailed).to.be.lengthOf(0, `Failed to load JSON for ${filesFailed.join(', ')}`); + }); +}); diff --git a/src/test/extension.unit.test.ts b/src/test/extension.unit.test.ts deleted file mode 100644 index c9693be71e91..000000000000 --- a/src/test/extension.unit.test.ts +++ /dev/null @@ -1,26 +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 { buildApi } from '../client/api'; -import { EXTENSION_ROOT_DIR } from '../client/common/constants'; - -const expectedPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'ptvsd_launcher.py'); - -suite('Extension API Debugger', () => { - test('Test debug launcher args (no-wait)', async () => { - const args = await buildApi(Promise.resolve()).debug.getRemoteLauncherCommand('something', 1234, false); - const expectedArgs = [expectedPath, '--default', '--host', 'something', '--port', '1234']; - expect(args).to.be.deep.equal(expectedArgs); - }); - test('Test debug launcher args (wait)', async () => { - const args = await buildApi(Promise.resolve()).debug.getRemoteLauncherCommand('something', 1234, true); - const expectedArgs = [expectedPath, '--default', '--host', 'something', '--port', '1234', '--wait']; - expect(args).to.be.deep.equal(expectedArgs); - }); -}); 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 { + return new vscode.EventEmitter().event; + } + + public autoSelectInterpreter(_resource: Resource): Promise { + return Promise.resolve(); + } + + public getAutoSelectedInterpreter(_resource: Resource): PythonEnvironment | undefined { + return undefined; + } + + public async setWorkspaceInterpreter( + _resource: Uri, + _interpreter: PythonEnvironment | undefined, + ): Promise { + 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 { + return fsextra.readFile(uri.fsPath); + } + + public async writeFile(uri: Uri, content: Uint8Array): Promise { + return fsextra.writeFile(uri.fsPath, Buffer.from(content)); + } + + public async delete(uri: Uri): Promise { + return ( + fsextra + // Make sure the file exists before deleting. + .stat(uri.fsPath) + .then(() => fsextra.remove(uri.fsPath)) + ); + } + + public async stat(uri: Uri): Promise { + 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 { + return fsextra.mkdirp(uri.fsPath); + } + + public async copy(src: Uri, dest: Uri): Promise { + const deferred = createDeferred(); + 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 { + return fsextra.rename(src.fsPath, dest.fsPath); + } +} diff --git a/src/test/fixtures.ts b/src/test/fixtures.ts new file mode 100644 index 000000000000..fbd8c20c9659 --- /dev/null +++ b/src/test/fixtures.ts @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +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'; + +export type CleanupFunc = (() => void) | (() => Promise); + +export class CleanupFixture { + private cleanups: CleanupFunc[]; + constructor() { + this.cleanups = []; + } + + public addCleanup(cleanup: CleanupFunc) { + this.cleanups.push(cleanup); + } + public addFSCleanup(filename: string) { + this.addCleanup(async () => { + try { + await fs.unlink(filename); + } catch { + // The file is already gone. + } + }); + } + + public async cleanUp() { + const cleanups = this.cleanups; + this.cleanups = []; + + return Promise.all( + cleanups.map(async (cleanup, i) => { + try { + const res = cleanup(); + if (res) { + await res; + } + } catch (err) { + console.error(`cleanup ${i + 1} failed: ${err}`); + + console.error('moving on...'); + } + }), + ); + } +} + +export class PythonFixture extends CleanupFixture { + public readonly python: string; + constructor( + // If not provided, we will use the global default. + python?: string, + ) { + super(); + if (python) { + this.python = python; + } else { + this.python = PYTHON_PATH; + } + } + + public runScript(filename: string, ...args: string[]): Proc { + return this.spawn(filename, ...args); + } + + public runModule(name: string, ...args: string[]): Proc { + return this.spawn('-m', name, ...args); + } + + private spawn(...args: string[]) { + const proc = spawn(this.python, ...args); + this.addCleanup(async () => { + if (!proc.exited) { + await sleep(1000); // Wait a sec before the hammer falls. + try { + proc.raw.kill(); + } catch { + // It already finished. + } + } + }); + return proc; + } +} diff --git a/src/test/format/extension.dispatch.test.ts b/src/test/format/extension.dispatch.test.ts deleted file mode 100644 index 711c8fd73435..000000000000 --- a/src/test/format/extension.dispatch.test.ts +++ /dev/null @@ -1,81 +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(); - const pos = TypeMoq.Mock.ofType(); - const opt = TypeMoq.Mock.ofType(); - const token = TypeMoq.Mock.ofType(); - const edits = TypeMoq.Mock.ofType>(); - - 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(); - const pos2 = TypeMoq.Mock.ofType(); - const opt2 = TypeMoq.Mock.ofType(); - const token2 = TypeMoq.Mock.ofType(); - const edits2 = TypeMoq.Mock.ofType>(); - - 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): TypeMoq.IMock { - const provider = TypeMoq.Mock.ofType(); - 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.test.ts b/src/test/format/extension.format.test.ts deleted file mode 100644 index addb7176f1b6..000000000000 --- a/src/test/format/extension.format.test.ts +++ /dev/null @@ -1,191 +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, IPythonExecutionFactory -} from '../../client/common/process/types'; -import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; -import { BlackFormatter } from '../../client/formatters/blackFormatter'; -import { YapfFormatter } from '../../client/formatters/yapfFormatter'; -import { isPythonVersionInProcess } from '../common'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; -import { MockProcessService } from '../mocks/proc'; -import { compareFiles } from '../textUtils'; -import { UnitTestIocContainer } from '../unittests/serviceRegistry'; - -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 blackFileToFormat = path.join(formatFilesPath, 'blackFileToFormat.py'); -const blackReferenceFile = path.join(formatFilesPath, 'blackFileReference.py'); -const yapfFileToFormat = path.join(formatFilesPath, 'yapfFileToFormat.py'); - -let formattedYapf = ''; -let formattedBlack = ''; -let formattedAutoPep8 = ''; - -// tslint:disable-next-line:max-func-body-length -suite('Formatting - General', () => { - let ioc: UnitTestIocContainer; - - suiteSetup(async () => { - await initialize(); - initializeDI(); - [autoPep8FileToFormat, blackFileToFormat, blackReferenceFile, yapfFileToFormat].forEach(file => { - fs.copySync(originalUnformattedFile, file, { overwrite: true }); - }); - fs.ensureDirSync(path.dirname(autoPep8FileToFormat)); - const pythonProcess = await ioc.serviceContainer.get(IPythonExecutionFactory).create({ resource: Uri.file(workspaceRootPath) }); - const yapf = pythonProcess.execModule('yapf', [originalUnformattedFile], { cwd: workspaceRootPath }); - const autoPep8 = pythonProcess.execModule('autopep8', [originalUnformattedFile], { cwd: workspaceRootPath }); - const formatters = [yapf, autoPep8]; - if (await formattingTestIsBlackSupported()) { - // Black doesn't support emitting only to stdout; it either works - // through a pipe, emits a diff, or rewrites the file in-place. - // Thus it's easier to let it do its in-place rewrite and then - // read the reference file from there. - const black = pythonProcess.execModule('black', [blackReferenceFile], { cwd: workspaceRootPath }); - formatters.push(black); - } - await Promise.all(formatters).then(async formattedResults => { - formattedYapf = formattedResults[0].stdout; - formattedAutoPep8 = formattedResults[1].stdout; - if (await formattingTestIsBlackSupported()) { - formattedBlack = fs.readFileSync(blackReferenceFile).toString(); - } - }); - }); - - async function formattingTestIsBlackSupported(): Promise { - const processService = await ioc.serviceContainer.get(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, blackReferenceFile, 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(); - - // Mocks. - ioc.registerMockProcessTypes(); - } - - async function injectFormatOutput(outputFileName: string) { - const procService = await ioc.serviceContainer.get(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 () => { - await testFormatting( - new AutoPep8Formatter(ioc.serviceContainer), - formattedAutoPep8, - autoPep8FileToFormat, - 'autopep8.output'); - }); - // tslint:disable-next-line:no-function-expression - test('Black', async function () { - 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.formatOnSave.test.ts b/src/test/format/extension.formatOnSave.test.ts deleted file mode 100644 index 0508a1a8474e..000000000000 --- a/src/test/format/extension.formatOnSave.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import * as vscode from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import { IConfigurationService } from '../../client/common/types'; -import { PythonFormattingEditProvider } from '../../client/providers/formatProvider'; -import { getExtensionSettings } from '../common'; -import { closeActiveWindows } from '../initialize'; -import { UnitTestIocContainer } from '../unittests/serviceRegistry'; - -const formatFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'formatting'); -const unformattedFile = path.join(formatFilesPath, 'fileToFormat.py'); - -suite('Formating On Save', () => { - let ioc: UnitTestIocContainer; - let config: TypeMoq.IMock; - let editorConfig: TypeMoq.IMock; - let workspace: TypeMoq.IMock; - let documentManager: TypeMoq.IMock; - let commands: TypeMoq.IMock; - let options: TypeMoq.IMock; - let listener: (d: vscode.TextDocument) => Promise; - - setup(initializeDI); - suiteTeardown(async () => { - await closeActiveWindows(); - }); - teardown(async () => { - await ioc.dispose(); - await closeActiveWindows(); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerFormatterTypes(); - ioc.registerFileSystemTypes(); - ioc.registerProcessTypes(); - ioc.registerVariableTypes(); - ioc.registerMockProcess(); - - config = TypeMoq.Mock.ofType(); - config.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => getExtensionSettings(undefined)); - - editorConfig = TypeMoq.Mock.ofType(); - - workspace = TypeMoq.Mock.ofType(); - workspace.setup(x => x.getConfiguration('editor', TypeMoq.It.isAny())).returns(() => editorConfig.object); - - const event = TypeMoq.Mock.ofType>(); - event.setup(x => x(TypeMoq.It.isAny())).callback((s) => { - listener = s as ((d: vscode.TextDocument) => Promise); - // tslint:disable-next-line:no-empty - }).returns(() => new vscode.Disposable(() => { })); - - documentManager = TypeMoq.Mock.ofType(); - documentManager.setup(x => x.onDidSaveTextDocument).returns(() => event.object); - - options = TypeMoq.Mock.ofType(); - options.setup(x => x.insertSpaces).returns(() => true); - options.setup(x => x.tabSize).returns(() => 4); - - commands = TypeMoq.Mock.ofType(); - commands.setup(x => x.executeCommand('editor.action.formatDocument')).returns(() => - new Promise((resolve, reject) => resolve()) - ); - ioc.serviceManager.addSingletonInstance(IConfigurationService, config.object); - ioc.serviceManager.addSingletonInstance(ICommandManager, commands.object); - ioc.serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); - ioc.serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); - } - - test('Workaround VS Code 41194', async () => { - editorConfig.setup(x => x.get('formatOnSave')).returns(() => true); - - const content = await fs.readFile(unformattedFile, 'utf8'); - let version = 1; - - const document = TypeMoq.Mock.ofType(); - document.setup(x => x.getText()).returns(() => content); - document.setup(x => x.uri).returns(() => vscode.Uri.file(unformattedFile)); - document.setup(x => x.isDirty).returns(() => false); - document.setup(x => x.fileName).returns(() => unformattedFile); - document.setup(x => x.save()).callback(() => version += 1); - document.setup(x => x.version).returns(() => version); - - const context = TypeMoq.Mock.ofType(); - const provider = new PythonFormattingEditProvider(context.object, ioc.serviceContainer); - const edits = await provider.provideDocumentFormattingEdits(document.object, options.object, new vscode.CancellationTokenSource().token); - expect(edits.length).be.greaterThan(0, 'Formatter produced no edits'); - - await listener(document.object); - await new Promise((resolve, reject) => setTimeout(resolve, 500)); - - commands.verify(x => x.executeCommand('editor.action.formatDocument'), TypeMoq.Times.once()); - document.verify(x => x.save(), TypeMoq.Times.once()); - }); -}); diff --git a/src/test/format/extension.lineFormatter.test.ts b/src/test/format/extension.lineFormatter.test.ts deleted file mode 100644 index 0cfaaaa180f4..000000000000 --- a/src/test/format/extension.lineFormatter.test.ts +++ /dev/null @@ -1,230 +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(); - document.setup(x => x.lineAt(TypeMoq.It.isAnyNumber())).returns(n => { - const line = TypeMoq.Mock.ofType(); - 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(); - line.setup(x => x.text).returns(() => text); - - const document = TypeMoq.Mock.ofType(); - 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 a19d26edc691..000000000000 --- a/src/test/format/extension.onEnterFormat.test.ts +++ /dev/null @@ -1,63 +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 06f449323836..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'; - -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 { - 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); - }); - }); -}); - -// tslint:disable-next-line:max-func-body-length -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); - }); - }); -}); - -// tslint:disable-next-line:max-func-body-length -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); - }); - }); -}); - -// tslint:disable-next-line:max-func-body-length -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 d96edbccc3e2..000000000000 --- a/src/test/format/extension.sort.test.ts +++ /dev/null @@ -1,116 +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 { commands, ConfigurationTarget, Position, Range, Uri, window, workspace } from 'vscode'; -import { Commands } from '../../client/common/constants'; -import { SortImportsEditingProvider } from '../../client/providers/importSortProvider'; -import { ISortImportsEditingProvider } from '../../client/providers/types'; -import { updateSetting } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { UnitTestIocContainer } from '../unittests/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('Sortingx', () => { - 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 () => { - 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(); - } - 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.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'); - }); -}); diff --git a/src/test/format/format.helper.test.ts b/src/test/format/format.helper.test.ts deleted file mode 100644 index 81edbf0fb10a..000000000000 --- a/src/test/format/format.helper.test.ts +++ /dev/null @@ -1,97 +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 '../unittests/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(); - config.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => getExtensionSettings(undefined)); - - ioc.serviceManager.addSingletonInstance(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(); - 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).forEach(product => { - const formatterMapping = new Map(); - 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/index.ts b/src/test/index.ts index 8df5640b079b..a4c69a2a9ac6 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,59 +1,35 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; -// tslint:disable-next-line:no-any +// Always place at the top, to ensure other modules are imported first. +require('./common/exitCIAfterTestReporter'); + if ((Reflect as any).metadata === undefined) { - // tslint:disable-next-line:no-require-imports no-var-requires require('reflect-metadata'); } -import { - IS_CI_SERVER_TEST_DEBUGGER, - IS_VSTS, MOCHA_CI_PROPERTIES, MOCHA_CI_REPORTER_ID, - MOCHA_CI_REPORTFILE, MOCHA_REPORTER_JUNIT -} from './ciConstants'; -import { IS_MULTI_ROOT_TEST } from './constants'; -import * as testRunner from './testRunner'; - -process.env.VSC_PYTHON_CI_TEST = '1'; -process.env.IS_MULTI_ROOT_TEST = IS_MULTI_ROOT_TEST.toString(); - -// If running on CI server and we're running the debugger tests, then ensure we only run debug tests. -// We do this to ensure we only run debugger test, as debugger tests are very flaky on CI. -// So the solution is to run them separately and first on CI. -const grep = IS_CI_SERVER_TEST_DEBUGGER ? 'Debug' : undefined; -const testFilesSuffix = process.env.TEST_FILES_SUFFIX; - -// You can directly control Mocha options by uncommenting the following lines. -// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info. -// Hack, as retries is not supported as setting in tsd. -const options: testRunner.SetupOptions & { retries: number } = { - ui: 'tdd', - useColors: true, - timeout: 25000, - retries: 3, - grep, - testFilesSuffix -}; +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, MAX_EXTENSION_ACTIVATION_TIME, TEST_RETRYCOUNT, TEST_TIMEOUT } from './constants'; +import { initialize } from './initialize'; +import { initializeLogger } from './testLogger'; -// VSTS CI doesn't display colours correctly (yet). -if (IS_VSTS) { - options.useColors = false; -} +initializeLogger(); -// CI can ask for a JUnit reporter if the environment variable -// 'MOCHA_REPORTER_JUNIT' is defined, further control is afforded -// by other 'MOCHA_CI_...' variables. See constants.ts for info. -if (MOCHA_REPORTER_JUNIT) { - options.reporter = MOCHA_CI_REPORTER_ID; - options.reporterOptions = { - mochaFile: MOCHA_CI_REPORTFILE, - properties: MOCHA_CI_PROPERTIES +type SetupOptions = Mocha.MochaOptions & { + testFilesSuffix: string; + reporterOptions?: { + mochaFile?: string; + properties?: string; }; -} + exit: boolean; +}; -process.on('unhandledRejection', (ex: string | Error, a) => { +process.on('unhandledRejection', (ex: any, _a) => { const message = [`${ex}`]; if (typeof ex !== 'string' && ex && ex.message) { message.push(ex.name); @@ -62,8 +38,128 @@ process.on('unhandledRejection', (ex: string | Error, a) => { message.push(ex.stack); } } - console.error(`Unhandled Promise Rejection with the message ${message.join(', ')}`); + + console.log(`Unhandled Promise Rejection with the message ${message.join(', ')}`); }); -testRunner.configure(options, { coverageConfig: '../coverconfig.json' }); -module.exports = testRunner; +/** + * Configure the test environment and return the optoins required to run moch tests. + */ +function configure(): SetupOptions { + process.env.VSC_PYTHON_CI_TEST = '1'; + process.env.IS_MULTI_ROOT_TEST = IS_MULTI_ROOT_TEST.toString(); + + // Check for a grep setting. Might be running a subset of the tests + const defaultGrep = process.env.VSC_PYTHON_CI_TEST_GREP; + // Check whether to invert the grep (i.e. test everything that doesn't include the grep). + const invert = (process.env.VSC_PYTHON_CI_TEST_INVERT_GREP || '').length > 0; + + // If running on CI server and we're running the debugger tests, then ensure we only run debug tests. + // We do this to ensure we only run debugger test, as debugger tests are very flaky on CI. + // So the solution is to run them separately and first on CI. + const grep = IS_CI_SERVER_TEST_DEBUGGER ? 'Debug' : defaultGrep; + const testFilesSuffix = process.env.TEST_FILES_SUFFIX || 'test'; + + const options: SetupOptions & { retries: number; invert: boolean } = { + ui: 'tdd', + invert, + timeout: TEST_TIMEOUT, + retries: TEST_RETRYCOUNT, + grep, + testFilesSuffix, + // Force Mocha to exit after tests. + // It has been observed that this isn't sufficient, hence the reason for src/test/common/exitCIAfterTestReporter.ts + exit: true, + }; + + // If the `MOCHA_REPORTER_JUNIT` env var is true, set up the CI reporter for + // reporting to both the console (spec) and to a JUnit XML file. The xml file + // written to is `test-report.xml` in the root folder by default, but can be + // changed by setting env var `MOCHA_FILE` (we do this in our CI). + if (MOCHA_REPORTER_JUNIT) { + options.reporter = 'mocha-multi-reporters'; + const reporterPath = path.join(__dirname, 'common', 'exitCIAfterTestReporter.js'); + options.reporterOptions = { + reporterEnabled: `spec,mocha-junit-reporter,${reporterPath}`, + }; + } + + // Linux: prevent a weird NPE when mocha on Linux requires the window size from the TTY. + // Since we are not running in a tty environment, we just implement the method statically. + const tty = require('tty'); + if (!tty.getWindowSize) { + tty.getWindowSize = () => [80, 75]; + } + + return options; +} + +/** + * Waits until the Python Extension completes loading or a timeout. + * When running tests within VSC, we need to wait for the Python Extension to complete loading, + * this is where `initialize` comes in, we load the PVSC extension using VSC API, wait for it + * to complete. + * That's when we know out PVSC extension specific code is ready for testing. + * So, this code needs to run always for every test running in VS Code (what we call these `system test`) . + */ +function activatePythonExtensionScript() { + const ex = new Error('Failed to initialize Python extension for tests after 3 minutes'); + 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]); + + promise.finally(() => clearTimeout(timer!)).catch((e) => console.error(e)); + return initializationPromise; +} + +/** + * Runner, invoked by VS Code. + * More info https://code.visualstudio.com/api/working-with-extensions/testing-extension + */ +export async function run(): Promise { + const options = configure(); + const mocha = new Mocha.default(options); + const testsRoot = path.join(__dirname); + + // Enable source map support. + require('source-map-support').install(); + + // 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((resolve, reject) => { + glob.default( + `**/**.${options.testFilesSuffix}.js`, + { ignore: ['**/**.unit.test.js', '**/**.functional.test.js'].concat(ignoreGlob), cwd: testsRoot }, + (error, files) => { + if (error) { + return reject(error); + } + resolve(files); + }, + ); + }); + + // Setup test files that need to be run. + testFiles.forEach((file) => mocha.addFile(path.join(testsRoot, file))); + + console.time('Time taken to activate the extension'); + try { + await activatePythonExtensionScript(); + console.timeEnd('Time taken to activate the extension'); + } catch (ex) { + console.error('Failed to activate python extension without errors', ex); + } + + // Run the tests. + await new Promise((resolve, reject) => { + mocha.run((failures) => { + if (failures > 0) { + return reject(new Error(`${failures} total failures`)); + } + resolve(); + }); + }); +} diff --git a/src/test/initialize.ts b/src/test/initialize.ts index 8090363452cb..0ed75a0aa5c1 100644 --- a/src/test/initialize.ts +++ b/src/test/initialize.ts @@ -1,20 +1,25 @@ -// tslint:disable:no-string-literal - import * as path from 'path'; import * as vscode from 'vscode'; -import { IExtensionApi } from '../client/api'; -import { clearPythonPathInWorkspaceFolder, IExtensionTestApi, PYTHON_PATH, resetGlobalPythonPathSetting, setPythonPathInWorkspaceRoot } from './common'; +import type { PythonExtension } from '../client/api/types'; +import { + clearPythonPathInWorkspaceFolder, + IExtensionTestApi, + PYTHON_PATH, + resetGlobalPythonPathSetting, + setPythonPathInWorkspaceRoot, +} from './common'; import { IS_SMOKE_TEST, PVSC_EXTENSION_ID_FOR_TESTS } from './constants'; +import { sleep } from './core'; export * from './constants'; export * from './ciConstants'; -const dummyPythonFile = path.join(__dirname, '..', '..', 'src', 'test', 'pythonFiles', 'dummy.py'); +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')); //First thing to be executed. -process.env['VSC_PYTHON_CI_TEST'] = '1'; +process.env.VSC_PYTHON_CI_TEST = '1'; // Ability to use custom python environments for testing export async function initializePython() { @@ -24,46 +29,85 @@ export async function initializePython() { await setPythonPathInWorkspaceRoot(PYTHON_PATH); } -// tslint:disable-next-line:no-any export async function initialize(): Promise { 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; + + return (api as any) as IExtensionTestApi; } export async function activateExtension() { - const extension = vscode.extensions.getExtension(PVSC_EXTENSION_ID_FOR_TESTS)!; + const extension = vscode.extensions.getExtension(PVSC_EXTENSION_ID_FOR_TESTS)!; const api = await extension.activate(); - // Wait untill its ready to use. + // Wait until its ready to use. await api.ready; return api; } -// tslint:disable-next-line:no-any + export async function initializeTest(): Promise { 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(); } } export async function closeActiveWindows(): Promise { + await closeActiveNotebooks(); + await closeWindowsInteral(); +} +export async function closeActiveNotebooks(): Promise { + if (!vscode.env.appName.toLowerCase().includes('insiders') || !isANotebookOpen()) { + return; + } + // We could have untitled notebooks, close them by reverting changes. + + 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). + // Hence keep trying. + for (let counter = 0; counter <= 5 && isANotebookOpen(); counter += 1) { + await sleep(counter * 100); + await closeWindowsInteral(); + } +} + +async function closeWindowsInteral() { return new Promise((resolve, reject) => { - vscode.commands.executeCommand('workbench.action.closeAllEditors') - // tslint:disable-next-line:no-unnecessary-callback-wrapper - .then(() => resolve(), reject); // Attempt to fix #1301. // Lets not waste too much time. - setTimeout(() => { - reject(new Error('Command \'workbench.action.closeAllEditors\' timed out')); + const timer = setTimeout(() => { + reject(new Error("Command 'workbench.action.closeAllEditors' timed out")); }, 15000); + vscode.commands.executeCommand('workbench.action.closeAllEditors').then( + () => { + clearTimeout(timer); + resolve(); + }, + (ex) => { + clearTimeout(timer); + reject(ex); + }, + ); }); } + +function isANotebookOpen() { + if (!vscode.window.activeTextEditor?.document) { + return false; + } + + 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 228c708d5603..e43fa21daf17 100644 --- a/src/test/install/channelManager.channels.test.ts +++ b/src/test/install/channelManager.channels.test.ts @@ -3,55 +3,45 @@ import * as assert from 'assert'; import { Container } from 'inversify'; -import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; -import { QuickPickOptions } from 'vscode'; import { IApplicationShell } from '../../client/common/application/types'; import { InstallationChannelManager } from '../../client/common/installer/channelManager'; import { IModuleInstaller } from '../../client/common/installer/types'; import { Product } from '../../client/common/types'; -import { Architecture } from '../../client/common/utils/platform'; -import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/types'; -import { IInterpreterLocatorService, InterpreterType, PIPENV_SERVICE, PythonInterpreter } from '../../client/interpreter/contracts'; +import { + IInterpreterAutoSelectionService, + IInterpreterAutoSelectionProxyService, +} from '../../client/interpreter/autoSelection/types'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; import { IServiceContainer } from '../../client/ioc/types'; 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; setup(() => { const cont = new Container(); serviceManager = new ServiceManager(cont); serviceContainer = new ServiceContainer(cont); - pipEnv = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IInterpreterLocatorService, pipEnv.object, PIPENV_SERVICE); - serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); + serviceManager.addSingleton( + IInterpreterAutoSelectionService, + MockAutoSelectionService, + ); + serviceManager.addSingleton( + IInterpreterAutoSelectionProxyService, + MockAutoSelectionService, + ); }); test('Single channel', async () => { const installer = mockInstaller(true, ''); const cm = new InstallationChannelManager(serviceContainer); const channels = await cm.getInstallationChannels(); - assert.equal(channels.length, 1, 'Incorrect number of channels'); - assert.equal(channels[0], installer.object, 'Incorrect installer'); + assert.strictEqual(channels.length, 1, 'Incorrect number of channels'); + assert.strictEqual(channels[0], installer.object, 'Incorrect installer'); }); test('Multiple channels', async () => { @@ -61,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 () => { @@ -72,53 +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(); + const appShell = createTypeMoq(); serviceManager.addSingletonInstance(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) => { + .setup((x) => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((i: string[]) => { items = i; }) - .returns(() => new Promise((resolve, reject) => resolve(undefined))); + .returns( + () => new Promise((resolve, _reject) => resolve(undefined)), + ); - installer1.setup(x => x.displayName).returns(() => 'Name 1'); - installer2.setup(x => x.displayName).returns(() => 'Name 2'); + 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 { - const installer = TypeMoq.Mock.ofType(); + const installer = createTypeMoq(); installer - .setup(x => x.isSupported(TypeMoq.It.isAny())) - .returns(() => new Promise((resolve) => resolve(supported))); - installer.setup(x => x.priority).returns(() => priority ? priority : 0); + .setup((x) => x.isSupported(TypeMoq.It.isAny())) + .returns( + () => new Promise((resolve) => resolve(supported)), + ); + installer.setup((x) => x.priority).returns(() => priority || 0); serviceManager.addSingletonInstance(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 394edb546714..1e9953b8b753 100644 --- a/src/test/install/channelManager.messages.test.ts +++ b/src/test/install/channelManager.messages.test.ts @@ -11,26 +11,30 @@ import { IModuleInstaller } from '../../client/common/installer/types'; import { IPlatformService } from '../../client/common/platform/types'; import { Product } from '../../client/common/types'; import { Architecture } from '../../client/common/utils/platform'; -import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/types'; -import { IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; +import { + IInterpreterAutoSelectionService, + 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 { 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; @@ -42,138 +46,147 @@ suite('Installation - channel messages', () => { const serviceManager = new ServiceManager(cont); serviceContainer = new ServiceContainer(cont); - platform = TypeMoq.Mock.ofType(); + platform = createTypeMoq(); serviceManager.addSingletonInstance(IPlatformService, platform.object); - appShell = TypeMoq.Mock.ofType(); + appShell = createTypeMoq(); serviceManager.addSingletonInstance(IApplicationShell, appShell.object); - interpreters = TypeMoq.Mock.ofType(); + interpreters = createTypeMoq(); serviceManager.addSingletonInstance(IInterpreterService, interpreters.object); - const moduleInstaller = TypeMoq.Mock.ofType(); + const moduleInstaller = createTypeMoq(); serviceManager.addSingletonInstance(IModuleInstaller, moduleInstaller.object); - serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); + serviceManager.addSingleton( + IInterpreterAutoSelectionService, + MockAutoSelectionService, + ); + serviceManager.addSingleton( + 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) => { - verifyMessage(message, ['Pip'], ['Conda']); - verifyUrl(url, ['Windows', 'Pip']); - }); + platform.setup((x) => x.isWindows).returns(() => true); + await testInstallerMissingMessage(EnvironmentType.Unknown, async (message: string, url: string) => { + verifyMessage(message, ['Pip'], ['Conda']); + verifyUrl(url, ['Windows', 'Pip']); + }); }); test('No installers message: Conda/Windows', async () => { - platform.setup(x => x.isWindows).returns(() => true); - await testInstallerMissingMessage(InterpreterType.Conda, - async (message: string, url: string) => { - verifyMessage(message, ['Pip', 'Conda'], []); - verifyUrl(url, ['Windows', 'Pip', 'Conda']); - }); + platform.setup((x) => x.isWindows).returns(() => true); + await testInstallerMissingMessage(EnvironmentType.Conda, async (message: string, url: string) => { + verifyMessage(message, ['Pip', 'Conda'], []); + verifyUrl(url, ['Windows', 'Pip', 'Conda']); + }); }); test('No installers message: Unknown/Mac', async () => { - platform.setup(x => x.isWindows).returns(() => false); - platform.setup(x => x.isMac).returns(() => true); - await testInstallerMissingMessage(InterpreterType.Unknown, - async (message: string, url: string) => { - verifyMessage(message, ['Pip'], ['Conda']); - verifyUrl(url, ['Mac', 'Pip']); - }); + platform.setup((x) => x.isWindows).returns(() => false); + platform.setup((x) => x.isMac).returns(() => true); + await testInstallerMissingMessage(EnvironmentType.Unknown, async (message: string, url: string) => { + verifyMessage(message, ['Pip'], ['Conda']); + verifyUrl(url, ['Mac', 'Pip']); + }); }); test('No installers message: Conda/Mac', async () => { - platform.setup(x => x.isWindows).returns(() => false); - platform.setup(x => x.isMac).returns(() => true); - await testInstallerMissingMessage(InterpreterType.Conda, - async (message: string, url: string) => { - verifyMessage(message, ['Pip', 'Conda'], []); - verifyUrl(url, ['Mac', 'Pip', 'Conda']); - }); + platform.setup((x) => x.isWindows).returns(() => false); + platform.setup((x) => x.isMac).returns(() => true); + await testInstallerMissingMessage(EnvironmentType.Conda, async (message: string, url: string) => { + verifyMessage(message, ['Pip', 'Conda'], []); + verifyUrl(url, ['Mac', 'Pip', 'Conda']); + }); }); test('No installers message: Unknown/Linux', async () => { - platform.setup(x => x.isWindows).returns(() => false); - platform.setup(x => x.isMac).returns(() => false); - platform.setup(x => x.isLinux).returns(() => true); - await testInstallerMissingMessage(InterpreterType.Unknown, - async (message: string, url: string) => { - verifyMessage(message, ['Pip'], ['Conda']); - verifyUrl(url, ['Linux', 'Pip']); - }); + platform.setup((x) => x.isWindows).returns(() => false); + platform.setup((x) => x.isMac).returns(() => false); + platform.setup((x) => x.isLinux).returns(() => true); + await testInstallerMissingMessage(EnvironmentType.Unknown, async (message: string, url: string) => { + verifyMessage(message, ['Pip'], ['Conda']); + verifyUrl(url, ['Linux', 'Pip']); + }); }); test('No installers message: Conda/Linux', async () => { - platform.setup(x => x.isWindows).returns(() => false); - platform.setup(x => x.isMac).returns(() => false); - platform.setup(x => x.isLinux).returns(() => true); - await testInstallerMissingMessage(InterpreterType.Conda, - async (message: string, url: string) => { - verifyMessage(message, ['Pip', 'Conda'], []); - verifyUrl(url, ['Linux', 'Pip', 'Conda']); - }); + platform.setup((x) => x.isWindows).returns(() => false); + platform.setup((x) => x.isMac).returns(() => false); + platform.setup((x) => x.isLinux).returns(() => true); + await testInstallerMissingMessage(EnvironmentType.Conda, async (message: string, url: string) => { + verifyMessage(message, ['Pip', 'Conda'], []); + verifyUrl(url, ['Linux', 'Pip', 'Conda']); + }); }); test('No channels message', async () => { - platform.setup(x => x.isWindows).returns(() => true); - await testInstallerMissingMessage(InterpreterType.Unknown, + platform.setup((x) => x.isWindows).returns(() => true); + await testInstallerMissingMessage( + EnvironmentType.Unknown, async (message: string, url: string) => { verifyMessage(message, ['Pip'], ['Conda']); verifyUrl(url, ['Windows', 'Pip']); - }, 'getInstallationChannel'); + }, + '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, - methodType: 'showNoInstallersMessage' | 'getInstallationChannel' = 'showNoInstallersMessage'): Promise { - - const activeInterpreter: PythonInterpreter = { + methodType: 'showNoInstallersMessage' | 'getInstallationChannel' = 'showNoInstallersMessage', + ): Promise { + const activeInterpreter: PythonEnvironment = { ...info, - type: interpreterType, - path: '' + envType: interpreterType, + path: '', }; interpreters - .setup(x => x.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => new Promise((resolve, reject) => resolve(activeInterpreter))); + .setup((x) => x.getActiveInterpreter(TypeMoq.It.isAny())) + .returns( + () => new Promise((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())) + .setup((x) => x.showErrorMessage(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) .callback((m: string, s: string) => { message = m; search = s; }) - .returns(() => new Promise((resolve, reject) => resolve(search))); - appShell.setup(x => x.openUrl(TypeMoq.It.isAnyString())).callback((s: string) => { - url = s; - }); + .returns( + () => new Promise((resolve, _reject) => resolve(search)), + ); + appShell + .setup((x) => x.openUrl(TypeMoq.It.isAnyString())) + .callback((s: string) => { + url = s; + }); if (methodType === 'showNoInstallersMessage') { await channels.showNoInstallersMessage(); } else { - await channels.getInstallationChannel(Product.pylint); + await 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; + let notificationEnabled: IPersistentState; + 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(); + terminalManager = mock(); + interpreterService = mock(); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + envName, + type, + } as unknown) as PythonEnvironment); + experimentService = mock(); + activeResourceService = mock(); + persistentStateFactory = mock(); + terminalEnvVarCollectionService = mock(); + configurationService = mock(); + when(configurationService.getSettings(anything())).thenReturn(({ + terminal: { + activateEnvironment: true, + }, + } as unknown) as IPythonSettings); + notificationEnabled = mock>(); + terminalEventEmitter = new EventEmitter(); + 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/service.unit.test.ts b/src/test/interpreters/activation/service.unit.test.ts new file mode 100644 index 000000000000..a0f9b3bd6915 --- /dev/null +++ b/src/test/interpreters/activation/service.unit.test.ts @@ -0,0 +1,357 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { expect } from 'chai'; +import { EOL } from 'os'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; +import { EventEmitter, Uri } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { WorkspaceService } from '../../../client/common/application/workspace'; +import { PlatformService } from '../../../client/common/platform/platformService'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { CurrentProcess } from '../../../client/common/process/currentProcess'; +import { ProcessService } from '../../../client/common/process/proc'; +import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; +import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { TerminalHelper } from '../../../client/common/terminal/helper'; +import { ITerminalHelper } from '../../../client/common/terminal/types'; +import { ICurrentProcess, 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'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterService } from '../../../client/interpreter/interpreterService'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +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, +}; + +suite('Interpreters Activation - Python Environment Variables', () => { + let service: EnvironmentActivationService; + let helper: ITerminalHelper; + let platform: IPlatformService; + let processServiceFactory: IProcessServiceFactory; + let processService: IProcessService; + let currentProcess: ICurrentProcess; + let envVarsService: IEnvironmentVariablesProvider; + let workspace: IWorkspaceService; + let interpreterService: IInterpreterService; + let onDidChangeEnvVariables: EventEmitter; + let onDidChangeInterpreter: EventEmitter; + const pythonInterpreter: PythonEnvironment = { + path: '/foo/bar/python.exe', + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, + }; + + function initSetup(interpreter: PythonEnvironment | undefined) { + helper = mock(TerminalHelper); + platform = mock(PlatformService); + processServiceFactory = mock(ProcessServiceFactory); + processService = mock(ProcessService); + currentProcess = mock(CurrentProcess); + envVarsService = mock(EnvironmentVariablesProvider); + interpreterService = mock(InterpreterService); + workspace = mock(WorkspaceService); + onDidChangeEnvVariables = new EventEmitter(); + onDidChangeInterpreter = new EventEmitter(); + 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), + instance(processServiceFactory), + instance(currentProcess), + instance(workspace), + instance(interpreterService), + instance(envVarsService), + ); + } + + function title(resource?: Uri, interpreter?: PythonEnvironment) { + return `${resource ? 'With a resource' : 'Without a resource'}${interpreter ? ' and an interpreter' : ''}`; + } + + [undefined, Uri.parse('a')].forEach((resource) => + [undefined, pythonInterpreter].forEach((interpreter) => { + suite(title(resource, interpreter), () => { + setup(() => initSetup(interpreter)); + test('Unknown os will return empty variables', async () => { + when(platform.osType).thenReturn(OSType.Unknown); + const env = await service.getActivatedEnvironmentVariables(resource); + + verify(platform.osType).once(); + expect(env).to.equal(undefined, 'Should not have any variables'); + }); + + const osTypes = getNamesAndValues(OSType).filter((osType) => osType.value !== OSType.Unknown); + + osTypes.forEach((osType) => { + suite(osType.name, () => { + setup(() => initSetup(interpreter)); + test('getEnvironmentActivationShellCommands will be invoked', async () => { + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).thenResolve(); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + + verify(platform.osType).once(); + expect(env).to.equal(undefined, 'Should not have any variables'); + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).once(); + }); + test('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 () => { + const cmd = ['1', '2']; + const envVars = { one: '1', two: '2' }; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve(envVars); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + + verify(platform.osType).once(); + expect(env).to.equal(undefined, 'Should not have any variables'); + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).once(); + verify(processServiceFactory.create(resource)).once(); + verify(envVarsService.getEnvironmentVariables(resource)).once(); + verify(processService.shellExec(anything(), anything())).once(); + + const shellCmd = capture(processService.shellExec).first()[0]; + + const printEnvPyFile = path.join( + EXTENSION_ROOT_DIR, + 'python_files', + 'printEnvVariables.py', + ); + const expectedCommand = [ + ...cmd, + `echo '${getEnvironmentPrefix}'`, + `python ${printEnvPyFile.fileToCommandArgumentForPythonExt()}`, + ].join(' && '); + + expect(shellCmd).to.equal(expectedCommand); + }); + test('Validate env Vars used to activation and printing env vars', async () => { + const cmd = ['1', '2']; + const envVars = { one: '1', two: '2' }; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve(envVars); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + + verify(platform.osType).once(); + expect(env).to.equal(undefined, 'Should not have any variables'); + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).once(); + verify(processServiceFactory.create(resource)).once(); + verify(envVarsService.getEnvironmentVariables(resource)).once(); + verify(processService.shellExec(anything(), anything())).once(); + + const options = capture(processService.shellExec).first()[1]; + + const expectedShell = defaultShells[osType.value]; + + expect(options).to.deep.equal({ + shell: expectedShell, + env: envVars, + timeout: 30000, + maxBuffer: 1000 * 1000, + throwOnStdErr: false, + }); + }); + test('Use current process variables if there are no custom variables', async () => { + const cmd = ['1', '2']; + const envVars = { one: '1', two: '2', PYTHONWARNINGS: 'ignore' }; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve({}); + when(currentProcess.env).thenReturn(envVars); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + + verify(platform.osType).once(); + expect(env).to.equal(undefined, 'Should not have any variables'); + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).once(); + verify(processServiceFactory.create(resource)).once(); + verify(envVarsService.getEnvironmentVariables(resource)).once(); + verify(processService.shellExec(anything(), anything())).once(); + verify(currentProcess.env).once(); + + const options = capture(processService.shellExec).first()[1]; + + const expectedShell = defaultShells[osType.value]; + + expect(options).to.deep.equal({ + env: envVars, + shell: expectedShell, + timeout: 30000, + maxBuffer: 1000 * 1000, + throwOnStdErr: false, + }); + }); + test('Error must be swallowed when activation fails', async () => { + const cmd = ['1', '2']; + const envVars = { one: '1', two: '2' }; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve(envVars); + when(processService.shellExec(anything(), anything())).thenReject(new Error('kaboom')); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + + verify(platform.osType).once(); + expect(env).to.equal(undefined, 'Should not have any variables'); + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).once(); + verify(processServiceFactory.create(resource)).once(); + verify(envVarsService.getEnvironmentVariables(resource)).once(); + verify(processService.shellExec(anything(), anything())).once(); + }); + test('Return parsed variables', async () => { + const cmd = ['1', '2']; + const envVars = { one: '1', two: '2' }; + const varsFromEnv = { one: '11', two: '22', HELLO: 'xxx' }; + const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve(envVars); + when(processService.shellExec(anything(), anything())).thenResolve({ stdout: stdout }); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + + verify(platform.osType).once(); + expect(env).to.deep.equal(varsFromEnv); + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).once(); + verify(processServiceFactory.create(resource)).once(); + verify(envVarsService.getEnvironmentVariables(resource)).once(); + verify(processService.shellExec(anything(), anything())).once(); + }); + test('Cache Variables', async () => { + const cmd = ['1', '2']; + const varsFromEnv = { one: '11', two: '22', HELLO: 'xxx' }; + const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve({}); + when(processService.shellExec(anything(), anything())).thenResolve({ stdout: stdout }); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + const env2 = await service.getActivatedEnvironmentVariables(resource, interpreter); + const env3 = await service.getActivatedEnvironmentVariables(resource, interpreter); + + expect(env).to.deep.equal(varsFromEnv); + // All same objects. + expect(env).to.equal(env2).to.equal(env3); + + // All methods invoked only once. + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).once(); + verify(processServiceFactory.create(resource)).once(); + verify(envVarsService.getEnvironmentVariables(resource)).once(); + verify(processService.shellExec(anything(), anything())).once(); + }); + async function testClearingCache(bustCache: Function) { + const cmd = ['1', '2']; + const varsFromEnv = { one: '11', two: '22', HELLO: 'xxx' }; + const stdout = `${getEnvironmentPrefix}${EOL}${JSON.stringify(varsFromEnv)}`; + when(platform.osType).thenReturn(osType.value); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).thenResolve(cmd); + when(processServiceFactory.create(resource)).thenResolve(instance(processService)); + when(envVarsService.getEnvironmentVariables(resource)).thenResolve({}); + when(processService.shellExec(anything(), anything())).thenResolve({ stdout: stdout }); + + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); + bustCache(); + const env2 = await service.getActivatedEnvironmentVariables(resource, interpreter); + + expect(env).to.deep.equal(varsFromEnv); + // Objects are different (not same reference). + expect(env).to.not.equal(env2); + // However variables are the same. + expect(env).to.deep.equal(env2); + + // All methods invoked twice as cache was blown. + verify( + helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter), + ).twice(); + verify(processServiceFactory.create(resource)).twice(); + verify(envVarsService.getEnvironmentVariables(resource)).twice(); + verify(processService.shellExec(anything(), anything())).twice(); + } + test('Cache Variables get cleared when changing 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; + 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(); + terminalDeactivateService = mock(); + 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(); + when(platform.osType).thenReturn(getOSType()); + interpreterService = mock(); + context = mock(); + shell = mock(); + const envVarProvider = mock(); + shellIntegrationService = mock(); + when(shellIntegrationService.isWorking()).thenResolve(true); + globalCollection = mock(); + collection = mock(); + when(context.environmentVariableCollection).thenReturn(instance(globalCollection)); + when(globalCollection.getScoped(anything())).thenReturn(instance(collection)); + experimentService = mock(); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); + applicationEnvironment = mock(); + when(applicationEnvironment.shell).thenReturn(customShell); + when(shell.withProgress(anything(), anything())) + .thenCall((options, _) => { + expect(options).to.deep.equal(progressOptions); + }) + .thenResolve(); + environmentActivationService = mock(); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + process.env, + ); + configService = mock(); + 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(); + 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; + 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; + 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/autoSelection/index.unit.test.ts b/src/test/interpreters/autoSelection/index.unit.test.ts index dbfe0eec6b52..6c5473546614 100644 --- a/src/test/interpreters/autoSelection/index.unit.test.ts +++ b/src/test/interpreters/autoSelection/index.unit.test.ts @@ -3,30 +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, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; +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 { 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 } from '../../../client/interpreter/autoSelection/types'; -import { IInterpreterHelper, PythonInterpreter } 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 { 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'; @@ -35,271 +37,652 @@ 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; + let state: PersistentState; let helper: IInterpreterHelper; - let proxy: IInterpreterAutoSeletionProxyService; + let proxy: IInterpreterAutoSelectionProxyService; + let interpreterService: IInterpreterService; + let experimentService: IExperimentService; + let sendTelemetryEventStub: sinon.SinonStub; + let telemetryEvents: { eventName: string; properties: Record }[] = []; class InterpreterAutoSelectionServiceTest extends InterpreterAutoSelectionService { - public initializeStore(): Promise { - return super.initializeStore(); + public initializeStore(resource: Resource): Promise { + return super.initializeStore(resource); } - public storeAutoSelectedInterperter(resource: Resource, interpreter: PythonInterpreter | undefined) { - return super.storeAutoSelectedInterperter(resource, interpreter); + + public storeAutoSelectedInterpreter(resource: Resource, interpreter: PythonEnvironment | undefined) { + return super.storeAutoSelectedInterpreter(resource, interpreter); + } + + public getAutoSelectedWorkspacePromises() { + return this.autoSelectedWorkspacePromises; } } setup(() => { workspaceService = mock(WorkspaceService); stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState); + state = mock(PersistentState) as PersistentState; fs = mock(FileSystem); - systemInterpreter = mock(SystemWideInterpretersAutoSelectionRule); - currentPathInterpreter = mock(CurrentPathInterpretersAutoSelectionRule); - winRegInterpreter = mock(WindowsRegistryInterpretersAutoSelectionRule); - cachedPaths = mock(CachedInterpretersAutoSelectionRule); - userDefinedInterpreter = mock(SettingsInterpretersAutoSelectionRule); - workspaceInterpreter = mock(WorkspaceVirtualEnvInterpretersAutoSelectionRule); helper = mock(InterpreterHelper); - proxy = mock(InterpreterAutoSeletionProxyService); + proxy = mock(InterpreterAutoSelectionProxyService); + interpreterService = mock(InterpreterService); + experimentService = mock(); + 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(proxy), instance(helper) + instance(workspaceService), + instance(stateFactory), + instance(fs), + instance(interpreterService), + interpreterComparer, + instance(proxy), + instance(helper), + instance(experimentService), ); - }); - test('Instance is registere in proxy', () => { - verify(proxy.registerInstance!(autoSelectionService)).once(); + 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, + ) => { + const telemetry = { eventName, properties }; + telemetryEvents.push(telemetry); + }) as typeof Telemetry.sendTelemetryEvent); }); - 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(); + + teardown(() => { + sinon.restore(); + Telemetry._resetSharedProperties(); + telemetryEvents = []; }); - test('Run rules in background', async () => { - autoSelectionService.initializeStore = () => Promise.resolve(); - await autoSelectionService.autoSelectInterpreter(undefined); - const allRules = [userDefinedInterpreter, winRegInterpreter, currentPathInterpreter, systemInterpreter, workspaceInterpreter, cachedPaths]; - for (const service of allRules) { - verify(service.autoSelectInterpreter(undefined)).once(); - if (service !== userDefinedInterpreter) { - verify(service.autoSelectInterpreter(anything(), autoSelectionService)).never(); - } - } - verify(userDefinedInterpreter.autoSelectInterpreter(anything(), autoSelectionService)).once(); + test('Instance is registered in proxy', () => { + verify(proxy.registerInstance!(autoSelectionService)).once(); }); - test('Run userDefineInterpreter as the first rule', async () => { - autoSelectionService.initializeStore = () => Promise.resolve(); - await autoSelectionService.autoSelectInterpreter(undefined); - verify(userDefinedInterpreter.autoSelectInterpreter(undefined, 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(anyString(), undefined), + ).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState( + '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; + when(globalQueriedState.value).thenReturn(true); + when(stateFactory.createGlobalPersistentState(anyString(), undefined)).thenReturn( + instance(globalQueriedState), + ); + + const queryState = mock(PersistentState) as PersistentState; + + when(queryState.value).thenReturn(undefined); + when(stateFactory.createWorkspacePersistentState(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; + + when(queryState.value).thenReturn(true); + when(stateFactory.createWorkspacePersistentState(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('Initialize the store', async () => { + const queryState = mock(PersistentState) as PersistentState; + + when(queryState.value).thenReturn(undefined); + when(stateFactory.createWorkspacePersistentState(anyString(), undefined)).thenReturn( + instance(queryState), + ); + when(queryState.value).thenReturn(undefined); + when(stateFactory.createGlobalPersistentState(anyString(), undefined)).thenReturn( + instance(queryState), + ); + let initialize = false; - autoSelectionService.initializeStore = async () => initialize = true as any; + let eventFired = false; + 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(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); - - await autoSelectionService.initializeStore(); - await autoSelectionService.initializeStore(); - await autoSelectionService.initializeStore(); - verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).once(); + test('Initializing the store would be executed once', async () => { + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.initializeStore(undefined); + + verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).twice(); }); - test('Clear file stored in cache if it doesn\'t exist', async () => { + + 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(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); when(state.value).thenReturn(interpreterInfo); when(fs.fileExists(pythonPath)).thenResolve(false); - await autoSelectionService.initializeStore(); + 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(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); when(state.value).thenReturn(interpreterInfo); when(fs.fileExists(pythonPath)).thenResolve(true); - await autoSelectionService.initializeStore(); + 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(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); - - await autoSelectionService.initializeStore(); - await autoSelectionService.storeAutoSelectedInterperter(undefined, interpreterInfo); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); + + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.storeAutoSelectedInterpreter(undefined, interpreterInfo); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); verify(state.updateValue(interpreterInfo)).once(); expect(selectedInterpreter).to.deep.equal(interpreterInfo); - expect(eventFired).to.deep.equal(true, 'event not fired'); + expect(eventFired).to.deep.equal(false, 'event fired'); }); + test('Do not store global interpreter info in state store when resource is undefined and version is lower than one already in state', async () => { let eventFired = false; const pythonPath = 'Hello World'; const interpreterInfo = { path: pythonPath, version: new SemVer('1.0.0') } as any; const interpreterInfoInState = { path: pythonPath, version: new SemVer('2.0.0') } as any; when(fs.fileExists(interpreterInfoInState.path)).thenResolve(true); - when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); when(state.value).thenReturn(interpreterInfoInState); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); - await autoSelectionService.initializeStore(); - await autoSelectionService.storeAutoSelectedInterperter(undefined, interpreterInfo); + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.storeAutoSelectedInterpreter(undefined, interpreterInfo); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); verify(state.updateValue(anything())).never(); expect(selectedInterpreter).to.deep.equal(interpreterInfoInState); expect(eventFired).to.deep.equal(false, 'event fired'); }); + test('Store global interpreter info in state store when resource is undefined and version is higher than one already in state', async () => { let eventFired = false; const pythonPath = 'Hello World'; const interpreterInfo = { path: pythonPath, version: new SemVer('3.0.0') } as any; const interpreterInfoInState = { path: pythonPath, version: new SemVer('2.0.0') } as any; when(fs.fileExists(interpreterInfoInState.path)).thenResolve(true); - when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); when(state.value).thenReturn(interpreterInfoInState); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); - await autoSelectionService.initializeStore(); - await autoSelectionService.storeAutoSelectedInterperter(undefined, interpreterInfo); + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.storeAutoSelectedInterpreter(undefined, interpreterInfo); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); verify(state.updateValue(anything())).once(); expect(selectedInterpreter).to.deep.equal(interpreterInfo); - expect(eventFired).to.deep.equal(true, 'event fired'); + 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(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); - - await autoSelectionService.initializeStore(); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); + + await autoSelectionService.initializeStore(undefined); await autoSelectionService.setGlobalInterpreter(interpreterInfo); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); verify(state.updateValue(interpreterInfo)).once(); expect(selectedInterpreter).to.deep.equal(interpreterInfo); }); + test('Store interpreter info in state store when resource is defined', async () => { let eventFired = false; const pythonPath = 'Hello World'; const interpreterInfo = { path: pythonPath } as any; const resource = Uri.parse('one'); - when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + 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(); - await autoSelectionService.storeAutoSelectedInterperter(resource, interpreterInfo); + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.storeAutoSelectedInterpreter(resource, interpreterInfo); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(resource); verify(state.updateValue(interpreterInfo)).never(); expect(selectedInterpreter).to.deep.equal(interpreterInfo); - expect(eventFired).to.deep.equal(true, 'event not fired'); + 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(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); + when(workspaceService.getWorkspaceFolderIdentifier(anything(), anything())).thenReturn(''); + const deferred = createDeferred(); + deferred.resolve(); + autoSelectionService.getAutoSelectedWorkspacePromises().set('', deferred); - await autoSelectionService.initializeStore(); + await autoSelectionService.initializeStore(undefined); await autoSelectionService.setWorkspaceInterpreter(resource, interpreterInfo); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(resource); - verify(state.updateValue(interpreterInfo)).never(); + 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(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); + when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); - await autoSelectionService.initializeStore(); - await autoSelectionService.storeAutoSelectedInterperter(resource, interpreterInfo); + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.storeAutoSelectedInterpreter(resource, interpreterInfo); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(undefined); verify(state.updateValue(interpreterInfo)).never(); expect(selectedInterpreter === null || selectedInterpreter === undefined).to.equal(true, 'Should be undefined'); }); + test('Return global value if we do not have a matching value for the resource', async () => { const pythonPath = 'Hello World'; const interpreterInfo = { path: pythonPath } as any; const resource = Uri.parse('one'); - when(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); - when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); + when( + stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); const globalInterpreterInfo = { path: 'global Value' }; when(state.value).thenReturn(globalInterpreterInfo as any); - await autoSelectionService.initializeStore(); - await autoSelectionService.storeAutoSelectedInterperter(resource, interpreterInfo); + when(workspaceService.getWorkspaceFolderIdentifier(resource, anything())).thenReturn('1'); + const deferred = createDeferred(); + deferred.resolve(); + autoSelectionService.getAutoSelectedWorkspacePromises().set('', deferred); + + await autoSelectionService.initializeStore(undefined); + await autoSelectionService.storeAutoSelectedInterpreter(resource, interpreterInfo); + const anotherResourceOfAnotherWorkspace = Uri.parse('Some other workspace'); + when(workspaceService.getWorkspaceFolderIdentifier(anotherResourceOfAnotherWorkspace, anything())).thenReturn( + '2', + ); + const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(anotherResourceOfAnotherWorkspace); - verify(workspaceService.getWorkspaceFolder(resource)).once(); - verify(workspaceService.getWorkspaceFolder(anotherResourceOfAnotherWorkspace)).once(); verify(state.updateValue(interpreterInfo)).never(); expect(selectedInterpreter).to.deep.equal(globalInterpreterInfo); }); - test('setWorkspaceInterpreter will invoke storeAutoSelectedInterperter with same args', async () => { - const pythonPath = 'Hello World'; - const interpreterInfo = { path: pythonPath } as any; - const resource = Uri.parse('one'); - const moq = typemoq.Mock.ofInstance(autoSelectionService, typemoq.MockBehavior.Loose, false); - moq - .setup(m => m.storeAutoSelectedInterperter(typemoq.It.isValue(resource), typemoq.It.isValue(interpreterInfo))) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - moq.callBase = true; - await moq.object.setWorkspaceInterpreter(resource, interpreterInfo); - - moq.verifyAll(); - }); - test('setGlobalInterpreter will invoke storeAutoSelectedInterperter with same args and without a resource', async () => { - const pythonPath = 'Hello World'; - const interpreterInfo = { path: pythonPath } as any; - const moq = typemoq.Mock.ofInstance(autoSelectionService, typemoq.MockBehavior.Loose, false); - moq - .setup(m => m.storeAutoSelectedInterperter(typemoq.It.isValue(undefined), typemoq.It.isValue(interpreterInfo))) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - moq.callBase = true; - await moq.object.setGlobalInterpreter(interpreterInfo); - - moq.verifyAll(); - }); }); diff --git a/src/test/interpreters/autoSelection/proxy.unit.test.ts b/src/test/interpreters/autoSelection/proxy.unit.test.ts index 576ee040fc5e..ff82725da57e 100644 --- a/src/test/interpreters/autoSelection/proxy.unit.test.ts +++ b/src/test/interpreters/autoSelection/proxy.unit.test.ts @@ -3,40 +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/interpreter/contracts'; +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(); - constructor(private readonly pythonPath: string = '') { } + + constructor(private readonly pythonPath: string = '') {} + public get onDidChangeAutoSelectedInterpreter(): Event { 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{ - return; + + // eslint-disable-next-line class-methods-use-this + public async setWorkspaceInterpreter(): Promise { + 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(); @@ -44,7 +51,7 @@ suite('Interpreters - Auto Selection Proxy', () => { expect(eventRaised).to.be.equal(true, 'Change event not fired'); }); - [undefined, Uri.parse('one')].forEach(resource => { + [undefined, Uri.parse('one')].forEach((resource) => { const suffix = resource ? '(with a resource)' : '(without a resource)'; test(`getAutoSelectedInterpreter should return undefined when instance isn't registered ${suffix}`, () => { @@ -58,6 +65,5 @@ suite('Interpreters - Auto Selection Proxy', () => { expect(value).to.be.deep.equal({ path: pythonPath }); }); - }); }); diff --git a/src/test/interpreters/autoSelection/rules/base.unit.test.ts b/src/test/interpreters/autoSelection/rules/base.unit.test.ts deleted file mode 100644 index b747634dd1fe..000000000000 --- a/src/test/interpreters/autoSelection/rules/base.unit.test.ts +++ /dev/null @@ -1,159 +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/interpreter/contracts'; - -suite('Interpreters - Auto Selection - Base Rule', () => { - let rule: BaseRuleServiceTest; - let stateFactory: IPersistentStateFactory; - let fs: IFileSystem; - let state: PersistentState; - class BaseRuleServiceTest extends BaseRuleService { - public async next(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { - 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 { - return super.setGlobalInterpreter(interpreter, manager); - } - protected async onAutoSelectInterpreter(_resource: Uri, _manager?: IInterpreterAutoSelectionService): Promise { - return NextAction.runNextRule; - } - } - setup(() => { - stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState); - fs = mock(FileSystem); - when(stateFactory.createGlobalPersistentState(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 interprter 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 e6900e8fdee1..000000000000 --- a/src/test/interpreters/autoSelection/rules/cached.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 { 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, PythonInterpreter } from '../../../../client/interpreter/contracts'; -import { InterpreterHelper } from '../../../../client/interpreter/helpers'; - -suite('Interpreters - Auto Selection - Cached Rule', () => { - let rule: CachedInterpretersAutoSelectionRuleTest; - let stateFactory: IPersistentStateFactory; - let fs: IFileSystem; - let state: PersistentState; - 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 { - return super.setGlobalInterpreter(interpreter, manager); - } - public async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { - 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(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 intepreters', 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 dad1d8fd457c..000000000000 --- a/src/test/interpreters/autoSelection/rules/currentPath.unit.test.ts +++ /dev/null @@ -1,101 +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, PythonInterpreter } from '../../../../client/interpreter/contracts'; -import { InterpreterHelper } from '../../../../client/interpreter/helpers'; -import { KnownPathsService } from '../../../../client/interpreter/locators/services/KnownPathsService'; - -suite('Interpreters - Auto Selection - Current Path Rule', () => { - let rule: CurrentPathInterpretersAutoSelectionRuleTest; - let stateFactory: IPersistentStateFactory; - let fs: IFileSystem; - let state: PersistentState; - let locator: IInterpreterLocatorService; - let helper: IInterpreterHelper; - class CurrentPathInterpretersAutoSelectionRuleTest extends CurrentPathInterpretersAutoSelectionRule { - public async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise { - return super.setGlobalInterpreter(interpreter, manager); - } - public async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { - return super.onAutoSelectInterpreter(resource, manager); - } - } - setup(() => { - stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState); - fs = mock(FileSystem); - helper = mock(InterpreterHelper); - locator = mock(KnownPathsService); - - when(stateFactory.createGlobalPersistentState(anything(), undefined)).thenReturn(instance(state)); - rule = new CurrentPathInterpretersAutoSelectionRuleTest(instance(fs), instance(helper), - instance(stateFactory), instance(locator)); - }); - - test('Invoke next rule if there are no intepreters 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 a1530b20c7e0..000000000000 --- a/src/test/interpreters/autoSelection/rules/settings.unit.test.ts +++ /dev/null @@ -1,77 +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 { 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 { SettingsInterpretersAutoSelectionRule } from '../../../../client/interpreter/autoSelection/rules/settings'; -import { IInterpreterAutoSelectionService } from '../../../../client/interpreter/autoSelection/types'; -import { PythonInterpreter } from '../../../../client/interpreter/contracts'; - -suite('Interpreters - Auto Selection - Settings Rule', () => { - let rule: SettingsInterpretersAutoSelectionRuleTest; - let stateFactory: IPersistentStateFactory; - let fs: IFileSystem; - let state: PersistentState; - let workspaceService: IWorkspaceService; - class SettingsInterpretersAutoSelectionRuleTest extends SettingsInterpretersAutoSelectionRule { - public async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { - return super.onAutoSelectInterpreter(resource, manager); - } - } - setup(() => { - stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState); - fs = mock(FileSystem); - workspaceService = mock(WorkspaceService); - - when(stateFactory.createGlobalPersistentState(anything(), undefined)).thenReturn(instance(state)); - rule = new SettingsInterpretersAutoSelectionRuleTest(instance(fs), - instance(stateFactory), instance(workspaceService)); - }); - - test('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('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('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 543415610aa0..000000000000 --- a/src/test/interpreters/autoSelection/rules/system.unit.test.ts +++ /dev/null @@ -1,108 +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, PythonInterpreter } from '../../../../client/interpreter/contracts'; -import { InterpreterHelper } from '../../../../client/interpreter/helpers'; -import { InterpreterService } from '../../../../client/interpreter/interpreterService'; - -suite('Interpreters - Auto Selection - System Interpreters Rule', () => { - let rule: SystemWideInterpretersAutoSelectionRuleTest; - let stateFactory: IPersistentStateFactory; - let fs: IFileSystem; - let state: PersistentState; - let interpreterService: IInterpreterService; - let helper: IInterpreterHelper; - class SystemWideInterpretersAutoSelectionRuleTest extends SystemWideInterpretersAutoSelectionRule { - public async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise { - return super.setGlobalInterpreter(interpreter, manager); - } - public async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { - return super.onAutoSelectInterpreter(resource, manager); - } - } - setup(() => { - stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState); - fs = mock(FileSystem); - helper = mock(InterpreterHelper); - interpreterService = mock(InterpreterService); - - when(stateFactory.createGlobalPersistentState(anything(), undefined)).thenReturn(instance(state)); - rule = new SystemWideInterpretersAutoSelectionRuleTest(instance(fs), instance(helper), - instance(stateFactory), instance(interpreterService)); - }); - - test('Invoke next rule if there are no intepreters 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 intepreters 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 intepreters 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 c5838a9015f6..000000000000 --- a/src/test/interpreters/autoSelection/rules/winRegistry.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-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, PythonInterpreter } from '../../../../client/interpreter/contracts'; -import { InterpreterHelper } from '../../../../client/interpreter/helpers'; -import { WindowsRegistryService } from '../../../../client/interpreter/locators/services/windowsRegistryService'; - -suite('Interpreters - Auto Selection - Windows Registry Rule', () => { - let rule: WindowsRegistryInterpretersAutoSelectionRuleTest; - let stateFactory: IPersistentStateFactory; - let fs: IFileSystem; - let state: PersistentState; - let locator: IInterpreterLocatorService; - let platform: IPlatformService; - let helper: IInterpreterHelper; - class WindowsRegistryInterpretersAutoSelectionRuleTest extends WindowsRegistryInterpretersAutoSelectionRule { - public async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise { - return super.setGlobalInterpreter(interpreter, manager); - } - public async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { - 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(anything(), undefined)).thenReturn(instance(state)); - rule = new WindowsRegistryInterpretersAutoSelectionRuleTest(instance(fs), instance(helper), - instance(stateFactory), instance(platform), instance(locator)); - }); - - getNamesAndValues(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 b033144fbb3e..000000000000 --- a/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts +++ /dev/null @@ -1,382 +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 { 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 { 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 { PythonPathUpdaterService } from '../../../../client/interpreter/configuration/pythonPathUpdaterService'; -import { IPythonPathUpdaterServiceManager } from '../../../../client/interpreter/configuration/types'; -import { IInterpreterHelper, IInterpreterLocatorService, PythonInterpreter, WorkspacePythonPath } from '../../../../client/interpreter/contracts'; -import { InterpreterHelper } from '../../../../client/interpreter/helpers'; -import { KnownPathsService } from '../../../../client/interpreter/locators/services/KnownPathsService'; - -suite('Interpreters - Auto Selection - Workspace Virtual Envs Rule', () => { - let rule: WorkspaceVirtualEnvInterpretersAutoSelectionRuleTest; - let stateFactory: IPersistentStateFactory; - let fs: IFileSystem; - let state: PersistentState; - let helper: IInterpreterHelper; - let platform: IPlatformService; - let pipEnvLocator: IInterpreterLocatorService; - let virtualEnvLocator: IInterpreterLocatorService; - let pythonPathUpdaterService: IPythonPathUpdaterServiceManager; - let workspaceService: IWorkspaceService; - class WorkspaceVirtualEnvInterpretersAutoSelectionRuleTest extends WorkspaceVirtualEnvInterpretersAutoSelectionRule { - public async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise { - return super.setGlobalInterpreter(interpreter, manager); - } - public async next(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { - return super.next(resource, manager); - } - public async cacheSelectedInterpreter(resource: Resource, interpreter: PythonInterpreter | undefined) { - return super.cacheSelectedInterpreter(resource, interpreter); - } - public async getWorkspaceVirtualEnvInterpreters(resource: Resource): Promise { - return super.getWorkspaceVirtualEnvInterpreters(resource); - } - } - setup(() => { - stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState); - fs = mock(FileSystem); - helper = mock(InterpreterHelper); - platform = mock(PlatformService); - pipEnvLocator = mock(KnownPathsService); - workspaceService = mock(WorkspaceService); - virtualEnvLocator = mock(KnownPathsService); - pythonPathUpdaterService = mock(PythonPathUpdaterService); - - when(stateFactory.createGlobalPersistentState(anything(), undefined)).thenReturn(instance(state)); - rule = new WorkspaceVirtualEnvInterpretersAutoSelectionRuleTest(instance(fs), instance(helper), - instance(stateFactory), instance(platform), - instance(workspaceService), instance(pythonPathUpdaterService), - instance(pipEnvLocator), instance(virtualEnvLocator)); - }); - 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); - type PythonPathInConfig = { workspaceFolderValue: string }; - const pythonPathInConfig = typemoq.Mock.ofType(); - 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('Does not udpate settings when there is no interpreter', async () => { - await rule.cacheSelectedInterpreter(undefined, {} as any); - - verify(pythonPathUpdaterService.updatePythonPath(anything(), anything(), anything(), anything())).never(); - }); - test('Does not udpate settings when there is not workspace', async () => { - const resource = Uri.file('x'); - when(helper.getActiveWorkspaceUri(resource)).thenReturn(undefined); - - await rule.cacheSelectedInterpreter(resource, {} as any); - - verify(pythonPathUpdaterService.updatePythonPath(anything(), anything(), anything(), anything())).never(); - verify(helper.getActiveWorkspaceUri(resource)).once(); - }); - test('Update settings', async () => { - const resource = Uri.file('x'); - const workspacePythonPath: WorkspacePythonPath = { configTarget: 'xyz' as any, folderUri: Uri.parse('folder') }; - const pythonPath = 'python Path to store in settings'; - when(helper.getActiveWorkspaceUri(resource)).thenReturn(workspacePythonPath); - - await rule.cacheSelectedInterpreter(resource, { path: pythonPath } as any); - - verify(pythonPathUpdaterService.updatePythonPath(pythonPath, workspacePythonPath.configTarget, 'load', workspacePythonPath.folderUri)).once(); - verify(helper.getActiveWorkspaceUri(resource)).once(); - }); - 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'); - - when(virtualEnvLocator.getInterpreters(resource, true)).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'); - - when(virtualEnvLocator.getInterpreters(resource, true)).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'); - - when(virtualEnvLocator.getInterpreters(resource, true)).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'); - - when(virtualEnvLocator.getInterpreters(resource, true)).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 pipEnv if that completes first with results', async () => { - const folderUri = Uri.parse('Folder'); - type PythonPathInConfig = { workspaceFolderValue: string }; - const pythonPathInConfig = typemoq.Mock.ofType(); - const pythonPath = { inspect: () => pythonPathInConfig.object }; - pythonPathInConfig - .setup(p => p.workspaceFolderValue) - .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 virtualEnvPromise = createDeferred(); - const nextInvoked = createDeferred(); - rule.next = () => Promise.resolve(nextInvoked.resolve()); - rule.getWorkspaceVirtualEnvInterpreters = () => virtualEnvPromise.promise; - when(pipEnvLocator.getInterpreters(folderUri)).thenResolve([interpreterInfo]); - when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); - - rule.cacheSelectedInterpreter = () => Promise.resolve(); - - await rule.autoSelectInterpreter(resource, instance(manager)); - virtualEnvPromise.resolve([]); - - expect(nextInvoked.completed).to.be.equal(true, 'Next rule not invoked'); - verify(helper.getActiveWorkspaceUri(resource)).atLeast(1); - verify(manager.setWorkspaceInterpreter(folderUri, interpreterInfo)).once(); - }); - test('Use virtualEnv if that completes first with results', async () => { - const folderUri = Uri.parse('Folder'); - type PythonPathInConfig = { workspaceFolderValue: string }; - const pythonPathInConfig = typemoq.Mock.ofType(); - const pythonPath = { inspect: () => pythonPathInConfig.object }; - pythonPathInConfig - .setup(p => p.workspaceFolderValue) - .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 pipEnvPromise = createDeferred(); - const nextInvoked = createDeferred(); - rule.next = () => Promise.resolve(nextInvoked.resolve()); - rule.getWorkspaceVirtualEnvInterpreters = () => Promise.resolve([interpreterInfo]); - when(pipEnvLocator.getInterpreters(folderUri)).thenResolve([interpreterInfo]); - when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); - - rule.cacheSelectedInterpreter = () => Promise.resolve(); - - await rule.autoSelectInterpreter(resource, instance(manager)); - pipEnvPromise.resolve([]); - - expect(nextInvoked.completed).to.be.equal(true, 'Next rule not invoked'); - verify(helper.getActiveWorkspaceUri(resource)).atLeast(1); - verify(manager.setWorkspaceInterpreter(folderUri, interpreterInfo)).once(); - }); - test('Wait for virtualEnv if pipEnv completes without any intepreters', async () => { - const folderUri = Uri.parse('Folder'); - type PythonPathInConfig = { workspaceFolderValue: string }; - const pythonPathInConfig = typemoq.Mock.ofType(); - const pythonPath = { inspect: () => pythonPathInConfig.object }; - pythonPathInConfig - .setup(p => p.workspaceFolderValue) - .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 manager = mock(InterpreterAutoSelectionService); - const resource = Uri.file('x'); - const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; - const virtualEnvPromise = createDeferred(); - const nextInvoked = createDeferred(); - rule.next = () => Promise.resolve(nextInvoked.resolve()); - rule.getWorkspaceVirtualEnvInterpreters = () => virtualEnvPromise.promise; - when(pipEnvLocator.getInterpreters(folderUri)).thenResolve([]); - when(helper.getBestInterpreter(deepEqual(anything()))).thenReturn(interpreterInfo); - - rule.cacheSelectedInterpreter = () => Promise.resolve(); - - setTimeout(() => virtualEnvPromise.resolve([interpreterInfo]), 10); - 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(); - }); - test('Wait for pipEnv if VirtualEnv completes without any intepreters', async () => { - const folderUri = Uri.parse('Folder'); - type PythonPathInConfig = { workspaceFolderValue: string }; - const pythonPathInConfig = typemoq.Mock.ofType(); - const pythonPath = { inspect: () => pythonPathInConfig.object }; - pythonPathInConfig - .setup(p => p.workspaceFolderValue) - .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 manager = mock(InterpreterAutoSelectionService); - const resource = Uri.file('x'); - const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; - const pipEnvPromise = createDeferred(); - const nextInvoked = createDeferred(); - rule.next = () => Promise.resolve(nextInvoked.resolve()); - rule.getWorkspaceVirtualEnvInterpreters = () => Promise.resolve([]); - when(pipEnvLocator.getInterpreters(folderUri)).thenResolve([]); - when(helper.getBestInterpreter(deepEqual(anything()))).thenReturn(interpreterInfo); - - rule.cacheSelectedInterpreter = () => Promise.resolve(); - - setTimeout(() => pipEnvPromise.resolve([interpreterInfo]), 10); - 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/condaEnvFileService.unit.test.ts b/src/test/interpreters/condaEnvFileService.unit.test.ts deleted file mode 100644 index 138809e7b261..000000000000 --- a/src/test/interpreters/condaEnvFileService.unit.test.ts +++ /dev/null @@ -1,98 +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 { ILogger, IPersistentStateFactory } from '../../client/common/types'; -import { ICondaService, IInterpreterHelper, IInterpreterLocatorService, InterpreterType } from '../../client/interpreter/contracts'; -import { AnacondaCompanyName } from '../../client/interpreter/locators/services/conda'; -import { CondaEnvFileService } from '../../client/interpreter/locators/services/condaEnvFileService'; -import { IServiceContainer } from '../../client/ioc/types'; -import { MockState } from './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 logger: TypeMoq.IMock; - let condaService: TypeMoq.IMock; - let interpreterHelper: TypeMoq.IMock; - let condaFileProvider: IInterpreterLocatorService; - let fileSystem: TypeMoq.IMock; - setup(() => { - const serviceContainer = TypeMoq.Mock.ofType(); - const stateFactory = TypeMoq.Mock.ofType(); - 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(); - interpreterHelper = TypeMoq.Mock.ofType(); - fileSystem = TypeMoq.Mock.ofType(); - logger = TypeMoq.Mock.ofType(); - condaFileProvider = new CondaEnvFileService(interpreterHelper.object, condaService.object, fileSystem.object, serviceContainer.object, logger.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/interpreters/condaEnvService.unit.test.ts b/src/test/interpreters/condaEnvService.unit.test.ts deleted file mode 100644 index 2b89594f19b4..000000000000 --- a/src/test/interpreters/condaEnvService.unit.test.ts +++ /dev/null @@ -1,356 +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 { ILogger, IPersistentStateFactory } from '../../client/common/types'; -import { ICondaService, InterpreterType } from '../../client/interpreter/contracts'; -import { InterpreterHelper } from '../../client/interpreter/helpers'; -import { AnacondaCompanyName } from '../../client/interpreter/locators/services/conda'; -import { CondaEnvService, parseCondaInfo } from '../../client/interpreter/locators/services/condaEnvService'; -import { IServiceContainer } from '../../client/ioc/types'; -import { UnitTestIocContainer } from '../unittests/serviceRegistry'; -import { MockState } from './mocks'; - -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 logger: TypeMoq.IMock; - let condaProvider: CondaEnvService; - let condaService: TypeMoq.IMock; - let interpreterHelper: TypeMoq.IMock; - let fileSystem: TypeMoq.IMock; - setup(() => { - initializeDI(); - const serviceContainer = TypeMoq.Mock.ofType(); - const stateFactory = TypeMoq.Mock.ofType(); - 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(); - interpreterHelper = TypeMoq.Mock.ofType(); - fileSystem = TypeMoq.Mock.ofType(); - condaProvider = new CondaEnvService(condaService.object, interpreterHelper.object, logger.object, serviceContainer.object, fileSystem.object); - }); - teardown(() => ioc.dispose()); - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerProcessTypes(); - logger = TypeMoq.Mock.ofType(); - } - - 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/interpreters/condaHelper.unit.test.ts b/src/test/interpreters/condaHelper.unit.test.ts deleted file mode 100644 index d151450d1a4a..000000000000 --- a/src/test/interpreters/condaHelper.unit.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import * as assert from 'assert'; -import { expect } from 'chai'; -import { CondaInfo } from '../../client/interpreter/contracts'; -import { AnacondaDisplayName } from '../../client/interpreter/locators/services/conda'; -import { CondaHelper } from '../../client/interpreter/locators/services/condaHelper'; - -// tslint:disable-next-line:max-func-body-length -suite('Interpreters display name from Conda Environments', () => { - const condaHelper = new CondaHelper(); - test('Must return default display name for invalid Conda Info', () => { - assert.equal(condaHelper.getDisplayName(), AnacondaDisplayName, 'Incorrect display name'); - assert.equal(condaHelper.getDisplayName({}), AnacondaDisplayName, 'Incorrect display name'); - }); - test('Must return at least Python Version', () => { - const info: CondaInfo = { - python_version: '3.6.1.final.10' - }; - const displayName = condaHelper.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 = condaHelper.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 = condaHelper.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 = condaHelper.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 -one1 /Users/donjayamanne/anaconda3/envs/one -two2 2 /Users/donjayamanne/anaconda3/envs/two 2 -three3 /Users/donjayamanne/anaconda3/envs/three - /Users/donjayamanne/anaconda3/envs/four - /Users/donjayamanne/anaconda3/envs/five 5`; - - const expectedList = [ - { name: 'base', path: '/Users/donjayamanne/anaconda3' }, - { name: 'one1', path: '/Users/donjayamanne/anaconda3/envs/one' }, - { name: 'two2 2', path: '/Users/donjayamanne/anaconda3/envs/two 2' }, - { name: 'three3', path: '/Users/donjayamanne/anaconda3/envs/three' }, - { name: 'four', path: '/Users/donjayamanne/anaconda3/envs/four' }, - { name: 'five 5', path: '/Users/donjayamanne/anaconda3/envs/five 5' } - ]; - - const list = condaHelper.parseCondaEnvironmentNames(environments); - expect(list).deep.equal(expectedList); - }); -}); diff --git a/src/test/interpreters/condaService.unit.test.ts b/src/test/interpreters/condaService.unit.test.ts deleted file mode 100644 index 9f05c2ded3ef..000000000000 --- a/src/test/interpreters/condaService.unit.test.ts +++ /dev/null @@ -1,712 +0,0 @@ -// 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 { FileSystem } from '../../client/common/platform/fileSystem'; -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, ILogger, IPersistentStateFactory, IPythonSettings } from '../../client/common/types'; -import { Architecture } from '../../client/common/utils/platform'; -import { - IInterpreterLocatorService, - IInterpreterService, - InterpreterType, - PythonInterpreter -} from '../../client/interpreter/contracts'; -import { CondaGetEnvironmentPrefix, CondaService } from '../../client/interpreter/locators/services/condaService'; -import { IServiceContainer } from '../../client/ioc/types'; -import { MockState } from './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: '' -}; - -suite('Interpreters Conda Service', () => { - let processService: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - let condaService: CondaService; - let fileSystem: TypeMoq.IMock; - let config: TypeMoq.IMock; - let settings: TypeMoq.IMock; - let registryInterpreterLocatorService: TypeMoq.IMock; - let serviceContainer: TypeMoq.IMock; - let procServiceFactory: TypeMoq.IMock; - let persistentStateFactory: TypeMoq.IMock; - let logger: TypeMoq.IMock; - let condaPathSetting: string; - let disposableRegistry: Disposable[]; - let interpreterService: TypeMoq.IMock; - let workspaceService : TypeMoq.IMock; - let mockState: MockState; - let terminalProvider: TypeMoq.IMock; - setup(async () => { - condaPathSetting = ''; - logger = TypeMoq.Mock.ofType(); - processService = TypeMoq.Mock.ofType(); - platformService = TypeMoq.Mock.ofType(); - persistentStateFactory = TypeMoq.Mock.ofType(); - interpreterService = TypeMoq.Mock.ofType(); - registryInterpreterLocatorService = TypeMoq.Mock.ofType(); - fileSystem = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); - config = TypeMoq.Mock.ofType(); - settings = TypeMoq.Mock.ofType(); - procServiceFactory = TypeMoq.Mock.ofType(); - 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(); - 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(); - 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(); - 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(ILogger), TypeMoq.It.isAny())).returns(() => logger.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) => { - return new FileSystem(platformService.object).arePathsSame(p1, p2); - }); - - condaService = new CondaService( - procServiceFactory.object, - platformService.object, - fileSystem.object, - persistentStateFactory.object, - config.object, - logger.object, - interpreterService.object, - disposableRegistry, - serviceContainer.object, - 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, - logger.object, - interpreterService.object, - disposableRegistry, - serviceContainer.object, - 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'))); - logger.setup(l => l.logInformation(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .verifiable(TypeMoq.Times.once()); - const environments = await condaService.getCondaEnvironments(true); - assert.equal(environments, undefined, 'Conda environments do not match'); - logger.verifyAll(); - }); - - 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); - }); - test('Create activated conda environment for Windows', async () => { - const pythonInterpreter: PythonInterpreter = { ...info, type: InterpreterType.Conda, envPath: 'C:\\Anaconda', envName: 'Anaconda' }; - const environment: any = { Path: 'C:\\test' }; - - platformService.setup(p => p.isWindows).returns(() => true); - fileSystem.setup(f => f.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); - - const newEnvironment = await condaService.getActivatedCondaEnvironment(pythonInterpreter, environment); - - // This part depends on the OS of path.join in getActivatedCondaEnvironment, so compute it here to match - const expectedPath = path.join(pythonInterpreter.envPath ? pythonInterpreter.envPath : '', 'Scripts'); - expect(newEnvironment.Path).to.be.equal(expectedPath.concat(';', environment.Path), 'Incorrect Windows Path Value'); - expect(newEnvironment.CONDA_PREFIX).to.be.equal(pythonInterpreter.envPath, 'Incorrect Windows CONDA_PREFIX Value'); - expect(newEnvironment.CONDA_DEFAULT_ENV).to.be.equal(pythonInterpreter.envName, 'Incorrect Windows CONDA_DEFAULT_ENV Value'); - }); - test('Create activated conda environment for Non-Windows', async () => { - const pythonInterpreter: PythonInterpreter = { ...info, type: InterpreterType.Conda, envPath: 'usr/Anaconda', envName: 'Anaconda' }; - const environment: any = { PATH: 'usr/test' }; - - platformService.setup(p => p.isWindows).returns(() => false); - fileSystem.setup(f => f.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); - - const newEnvironment = await condaService.getActivatedCondaEnvironment(pythonInterpreter, environment); - - // This part depends on the OS of path.join in getActivatedCondaEnvironment, so compute it here to match - const expectedPath = path.join('usr/Anaconda', 'bin'); - expect(newEnvironment.PATH).to.be.equal(expectedPath.concat(':', environment.PATH), 'Incorrect Non-Windows Path Value'); - expect(newEnvironment.CONDA_PREFIX).to.be.equal(pythonInterpreter.envPath, 'Incorrect Non-Windows CONDA_PREFIX Value'); - expect(newEnvironment.CONDA_DEFAULT_ENV).to.be.equal(pythonInterpreter.envName, 'Incorrect Non-Windows CONDA_DEFAULT_ENV Value'); - }); - test('Create activated conda environment for using shell', async () => { - const pythonInterpreter: PythonInterpreter = { ...info, type: InterpreterType.Conda, envPath: 'C:\\Anaconda\\Foo\\envs\\test', envName: 'Anaconda' }; - const environment: any = { Path: 'C:\\test' }; - - platformService.setup(p => p.isWindows).returns(() => true); - fileSystem.setup(f => f.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); - processService.setup(p => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: `${CondaGetEnvironmentPrefix}\r\nCONDA_PREFIX=TEST_PREFIX` })); - - const newEnvironment = await condaService.getActivatedCondaEnvironment(pythonInterpreter, environment); - - expect(newEnvironment.CONDA_PREFIX).to.be.equal('TEST_PREFIX', 'Incorrect shell exec CONDA_PREFIX Value'); - }); - test('Create activated conda environment for using shell that fails all', async () => { - const pythonInterpreter: PythonInterpreter = { ...info, type: InterpreterType.Conda, envPath: 'C:\\Anaconda\\Foo\\envs\\test', envName: 'Anaconda' }; - const environment: any = { Path: 'C:\\test' }; - - platformService.setup(p => p.isWindows).returns(() => true); - fileSystem.setup(f => f.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); - processService.setup(p => p.shellExec(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.reject()); - - const newEnvironment = await condaService.getActivatedCondaEnvironment(pythonInterpreter, environment); - - // This part depends on the OS of path.join in getActivatedCondaEnvironment, so compute it here to match - const expectedPath = path.join(pythonInterpreter.envPath ? pythonInterpreter.envPath : '', 'Scripts'); - expect(newEnvironment.Path).to.be.equal(expectedPath.concat(';', environment.Path), 'Incorrect Windows Path Value'); - expect(newEnvironment.CONDA_PREFIX).to.be.equal(pythonInterpreter.envPath, 'Incorrect Windows CONDA_PREFIX Value'); - expect(newEnvironment.CONDA_DEFAULT_ENV).to.be.equal(pythonInterpreter.envName, 'Incorrect Windows CONDA_DEFAULT_ENV Value'); - }); - -}); diff --git a/src/test/interpreters/currentPathService.unit.test.ts b/src/test/interpreters/currentPathService.unit.test.ts deleted file mode 100644 index b079d339495d..000000000000 --- a/src/test/interpreters/currentPathService.unit.test.ts +++ /dev/null @@ -1,91 +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 { 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, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; -import { InterpreterHelper } from '../../client/interpreter/helpers'; -import { CurrentPathService, PythonInPathCommandProvider } from '../../client/interpreter/locators/services/currentPathService'; -import { IPythonInPathCommandProvider } from '../../client/interpreter/locators/types'; -import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; -import { IServiceContainer } from '../../client/ioc/types'; - -suite('Interpreters CurrentPath Service', () => { - let processService: TypeMoq.IMock; - let fileSystem: TypeMoq.IMock; - let serviceContainer: TypeMoq.IMock; - let virtualEnvironmentManager: TypeMoq.IMock; - let interpreterHelper: TypeMoq.IMock; - let pythonSettings: TypeMoq.IMock; - let currentPathService: CurrentPathService; - let persistentState: TypeMoq.IMock>; - let platformService: TypeMoq.IMock; - let pythonInPathCommandProvider: IPythonInPathCommandProvider; - setup(async () => { - processService = TypeMoq.Mock.ofType(); - virtualEnvironmentManager = TypeMoq.Mock.ofType(); - interpreterHelper = TypeMoq.Mock.ofType(); - const configurationService = TypeMoq.Mock.ofType(); - pythonSettings = TypeMoq.Mock.ofType(); - configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - const persistentStateFactory = TypeMoq.Mock.ofType(); - persistentState = TypeMoq.Mock.ofType>(); - 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(); - platformService = TypeMoq.Mock.ofType(); - persistentStateFactory.setup(p => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => persistentState.object); - const procServiceFactory = TypeMoq.Mock.ofType(); - procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); - - serviceContainer = TypeMoq.Mock.ofType(); - 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 = ['-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 dc9090525ecc..d9be806ff709 100644 --- a/src/test/interpreters/display.unit.test.ts +++ b/src/test/interpreters/display.unit.test.ts @@ -1,29 +1,49 @@ import { expect } from 'chai'; import * as path from 'path'; import { SemVer } from 'semver'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Disposable, StatusBarAlignment, StatusBarItem, Uri, WorkspaceFolder } from 'vscode'; +import { + ConfigurationTarget, + Disposable, + EventEmitter, + LanguageStatusItem, + LanguageStatusSeverity, + StatusBarAlignment, + StatusBarItem, + Uri, + WorkspaceFolder, +} from 'vscode'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; +import { Commands, PYTHON_LANGUAGE } from '../../client/common/constants'; import { IFileSystem } from '../../client/common/platform/types'; -import { IConfigurationService, IDisposableRegistry, IPathUtils, IPythonSettings } from '../../client/common/types'; +import { IDisposableRegistry, IPathUtils, ReadWrite } from '../../client/common/types'; +import { InterpreterQuickPickList } from '../../client/common/utils/localize'; import { Architecture } from '../../client/common/utils/platform'; -import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; +import { + IInterpreterDisplay, + IInterpreterHelper, + IInterpreterService, + IInterpreterStatusbarVisibilityFilter, +} from '../../client/interpreter/contracts'; import { InterpreterDisplay } from '../../client/interpreter/display'; -import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; import { IServiceContainer } from '../../client/ioc/types'; +import * 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'; -// tslint:disable:no-any max-func-body-length - -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', () => { @@ -31,140 +51,403 @@ suite('Interpreters Display', () => { let workspaceService: TypeMoq.IMock; let serviceContainer: TypeMoq.IMock; let interpreterService: TypeMoq.IMock; - let virtualEnvMgr: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; let disposableRegistry: Disposable[]; let statusBar: TypeMoq.IMock; - let pythonSettings: TypeMoq.IMock; - let configurationService: TypeMoq.IMock; - let interpreterDisplay: IInterpreterDisplay; + let interpreterDisplay: IInterpreterDisplay & IExtensionSingleActivationService; let interpreterHelper: TypeMoq.IMock; let pathUtils: TypeMoq.IMock; - setup(() => { + let languageStatusItem: TypeMoq.IMock; + 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(); workspaceService = TypeMoq.Mock.ofType(); applicationShell = TypeMoq.Mock.ofType(); interpreterService = TypeMoq.Mock.ofType(); - virtualEnvMgr = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); interpreterHelper = TypeMoq.Mock.ofType(); disposableRegistry = []; statusBar = TypeMoq.Mock.ofType(); - pythonSettings = TypeMoq.Mock.ofType(); - configurationService = TypeMoq.Mock.ofType(); + statusBar.setup((s) => s.name).returns(() => ''); + languageStatusItem = TypeMoq.Mock.ofType(); pathUtils = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => applicationShell.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterService))).returns(() => interpreterService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IVirtualEnvironmentManager))).returns(() => virtualEnvMgr.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => disposableRegistry); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configurationService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))).returns(() => interpreterHelper.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); + traceLogStub = sinon.stub(logging, 'traceLog'); - applicationShell.setup(a => a.createStatusBarItem(TypeMoq.It.isValue(StatusBarAlignment.Left), TypeMoq.It.isValue(100))).returns(() => statusBar.object); - pathUtils.setup(p => p.getDisplayName(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(p => p); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) + .returns(() => applicationShell.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => disposableRegistry); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterHelper))) + .returns(() => interpreterHelper.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.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); + await createInterpreterDisplay(); + } - interpreterDisplay = new InterpreterDisplay(serviceContainer.object); - }); function setupWorkspaceFolder(resource: Uri, workspaceFolder?: Uri) { if (workspaceFolder) { const mockFolder = TypeMoq.Mock.ofType(); - mockFolder.setup(w => w.uri).returns(() => workspaceFolder); - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))).returns(() => mockFolder.object); + mockFolder.setup((w) => w.uri).returns(() => workspaceFolder); + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))) + .returns(() => mockFolder.object); } else { - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))).returns(() => undefined); + workspaceService.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))).returns(() => undefined); } } - test('Sattusbar 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); - 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); - - 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.once()); - }); - 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.once()); - 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.once()); + [false].forEach((useLanguageStatus) => { + suite(`When ${useLanguageStatus ? `using language status` : 'using status bar'}`, () => { + setup(async () => { + setupMocks(useLanguageStatus); + }); + + 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); + + 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); + 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'; + + 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); + 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); + + 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(); + const filter1: ReadWrite = { + hidden: false, + changed: event1.event, + }; + const event2 = new EventEmitter(); + const filter2: ReadWrite = { + 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/progressDisplay.unit.test.ts b/src/test/interpreters/display/progressDisplay.unit.test.ts index 1cd5037501ad..b1acecd44434 100644 --- a/src/test/interpreters/display/progressDisplay.unit.test.ts +++ b/src/test/interpreters/display/progressDisplay.unit.test.ts @@ -3,80 +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'; - -type ProgressTask = (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable; +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 = ( + progress: Progress<{ message?: string; increment?: number }>, + token: CancellationToken, +) => Thenable; 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; + let componentAdapter: IComponentAdapter; + setup(() => { + refreshDeferred = createDeferred(); + 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; - 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; + 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 0027bf096782..3f64d5a26580 100644 --- a/src/test/interpreters/helpers.unit.test.ts +++ b/src/test/interpreters/helpers.unit.test.ts @@ -8,35 +8,41 @@ 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 { IServiceContainer } from '../../client/ioc/types'; -// tslint:disable:max-func-body-length no-any suite('Interpreters Display Helper', () => { let documentManager: TypeMoq.IMock; let workspaceService: TypeMoq.IMock; let serviceContainer: TypeMoq.IMock; let helper: InterpreterHelper; + let pyenvs: TypeMoq.IMock; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); workspaceService = TypeMoq.Mock.ofType(); documentManager = TypeMoq.Mock.ofType(); + pyenvs = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager))).returns(() => documentManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager))) + .returns(() => documentManager.object); - helper = new InterpreterHelper(serviceContainer.object); + helper = new InterpreterHelper(serviceContainer.object, pyenvs.object); }); test('getActiveWorkspaceUri should return undefined if there are no workspaces', () => { - workspaceService.setup(w => w.workspaceFolders).returns(() => []); - documentManager.setup(doc => doc.activeTextEditor).returns(() => undefined); + workspaceService.setup((w) => w.workspaceFolders).returns(() => []); + documentManager.setup((doc) => doc.activeTextEditor).returns(() => undefined); const workspace = helper.getActiveWorkspaceUri(undefined); expect(workspace).to.be.equal(undefined, 'incorrect value'); }); test('getActiveWorkspaceUri should return the workspace if there is only one', () => { const folderUri = Uri.file('abc'); - // tslint:disable-next-line:no-any - workspaceService.setup(w => w.workspaceFolders).returns(() => [{ uri: folderUri } as any]); + + workspaceService.setup((w) => w.workspaceFolders).returns(() => [{ uri: folderUri } as any]); const workspace = helper.getActiveWorkspaceUri(undefined); expect(workspace).to.be.not.equal(undefined, 'incorrect value'); @@ -45,9 +51,9 @@ 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); + + workspaceService.setup((w) => w.workspaceFolders).returns(() => [{ uri: folderUri } as any, undefined as any]); + documentManager.setup((d) => d.activeTextEditor).returns(() => undefined); const workspace = helper.getActiveWorkspaceUri(undefined); expect(workspace).to.be.equal(undefined, 'incorrect value'); @@ -55,14 +61,14 @@ 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]); + + workspaceService.setup((w) => w.workspaceFolders).returns(() => [{ uri: folderUri } as any, undefined as any]); const textEditor = TypeMoq.Mock.ofType(); const document = TypeMoq.Mock.ofType(); - textEditor.setup(t => t.document).returns(() => document.object); - document.setup(d => d.uri).returns(() => documentUri); - documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(documentUri))).returns(() => undefined); + textEditor.setup((t) => t.document).returns(() => document.object); + document.setup((d) => d.uri).returns(() => documentUri); + documentManager.setup((d) => d.activeTextEditor).returns(() => textEditor.object); + workspaceService.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(documentUri))).returns(() => undefined); const workspace = helper.getActiveWorkspaceUri(undefined); expect(workspace).to.be.equal(undefined, 'incorrect value'); @@ -71,15 +77,19 @@ 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]); + + workspaceService.setup((w) => w.workspaceFolders).returns(() => [{ uri: folderUri } as any, undefined as any]); const textEditor = TypeMoq.Mock.ofType(); const document = TypeMoq.Mock.ofType(); - textEditor.setup(t => t.document).returns(() => document.object); - document.setup(d => d.uri).returns(() => documentUri); - documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); - // tslint:disable-next-line:no-any - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(documentUri))).returns(() => { return { uri: documentWorkspaceFolderUri } as any; }); + textEditor.setup((t) => t.document).returns(() => document.object); + document.setup((d) => d.uri).returns(() => documentUri); + documentManager.setup((d) => d.activeTextEditor).returns(() => textEditor.object); + + workspaceService + .setup((w) => w.getWorkspaceFolder(TypeMoq.It.isValue(documentUri))) + .returns(() => { + return { uri: documentWorkspaceFolderUri } as any; + }); const workspace = helper.getActiveWorkspaceUri(undefined); expect(workspace).to.be.not.equal(undefined, 'incorrect value'); 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(); + 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().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, + ); + 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 21bf8cfbcc68..1d521dad8ec8 100644 --- a/src/test/interpreters/interpreterService.unit.test.ts +++ b/src/test/interpreters/interpreterService.unit.test.ts @@ -3,365 +3,306 @@ 'use strict'; -// tslint:disable:max-func-body-length no-any - import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { Container } from 'inversify'; import * as path from 'path'; -import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; -import { Disposable, TextDocument, TextEditor, Uri, WorkspaceConfiguration } from 'vscode'; -import { IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; -import { getArchitectureDisplayName } from '../../client/common/platform/registry'; +import * as sinon from 'sinon'; +import { ConfigurationTarget, Disposable, TextDocument, TextEditor, Uri, WorkspaceConfiguration } from 'vscode'; +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, IPersistentStateFactory, IPythonSettings } from '../../client/common/types'; -import * as EnumEx from '../../client/common/utils/enum'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IInstaller, + IInterpreterPathService, + InterpreterConfigurationScope, + IPersistentStateFactory, + IPythonSettings, +} from '../../client/common/types'; import { noop } from '../../client/common/utils/misc'; -import { Architecture } from '../../client/common/utils/platform'; -import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/types'; -import { IPythonPathUpdaterServiceManager } from '../../client/interpreter/configuration/types'; import { - IInterpreterDisplay, - IInterpreterHelper, - IInterpreterLocatorService, - INTERPRETER_LOCATOR_SERVICE, - InterpreterType, - PythonInterpreter -} from '../../client/interpreter/contracts'; + IInterpreterAutoSelectionService, + IInterpreterAutoSelectionProxyService, +} from '../../client/interpreter/autoSelection/types'; +import { IPythonPathUpdaterServiceManager } from '../../client/interpreter/configuration/types'; +import { IComponentAdapter, IInterpreterDisplay, IInterpreterHelper } from '../../client/interpreter/contracts'; import { InterpreterService } from '../../client/interpreter/interpreterService'; -import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; import { ServiceContainer } from '../../client/ioc/container'; import { ServiceManager } from '../../client/ioc/serviceManager'; 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'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ -use(chaiAsPromised); +use(chaiAsPromised.default); suite('Interpreters service', () => { let serviceManager: ServiceManager; let serviceContainer: ServiceContainer; let updater: TypeMoq.IMock; + let pyenvs: TypeMoq.IMock; let helper: TypeMoq.IMock; - let locator: TypeMoq.IMock; let workspace: TypeMoq.IMock; let config: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; let interpreterDisplay: TypeMoq.IMock; - let virtualEnvMgr: TypeMoq.IMock; let persistentStateFactory: TypeMoq.IMock; let pythonExecutionFactory: TypeMoq.IMock; let pythonExecutionService: TypeMoq.IMock; let configService: TypeMoq.IMock; + let interpreterPathService: TypeMoq.IMock; let pythonSettings: TypeMoq.IMock; + let experiments: TypeMoq.IMock; + let installer: TypeMoq.IMock; + let appShell: TypeMoq.IMock; + 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); - updater = TypeMoq.Mock.ofType(); - helper = TypeMoq.Mock.ofType(); - locator = TypeMoq.Mock.ofType(); - workspace = TypeMoq.Mock.ofType(); - config = TypeMoq.Mock.ofType(); - fileSystem = TypeMoq.Mock.ofType(); - interpreterDisplay = TypeMoq.Mock.ofType(); - virtualEnvMgr = TypeMoq.Mock.ofType(); - persistentStateFactory = TypeMoq.Mock.ofType(); - pythonExecutionFactory = TypeMoq.Mock.ofType(); - pythonExecutionService = TypeMoq.Mock.ofType(); - configService = TypeMoq.Mock.ofType(); - - pythonSettings = TypeMoq.Mock.ofType(); - pythonSettings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + interpreterPathService = createTypeMoq(); + updater = createTypeMoq(); + pyenvs = createTypeMoq(); + helper = createTypeMoq(); + workspace = createTypeMoq(); + config = createTypeMoq(); + fileSystem = createTypeMoq(); + interpreterDisplay = createTypeMoq(); + persistentStateFactory = createTypeMoq(); + pythonExecutionFactory = createTypeMoq(); + pythonExecutionService = createTypeMoq(); + configService = createTypeMoq(); + installer = createTypeMoq(); + appShell = createTypeMoq(); + experiments = createTypeMoq(); + + pythonSettings = createTypeMoq(); + pythonSettings.setup((s) => s.pythonPath).returns(() => PYTHON_PATH); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); pythonExecutionService.setup((p: any) => p.then).returns(() => undefined); - workspace.setup(x => x.getConfiguration('python', TypeMoq.It.isAny())).returns(() => config.object); - pythonExecutionFactory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(pythonExecutionService.object)); - fileSystem.setup(fs => fs.getFileHash(TypeMoq.It.isAny())).returns(() => Promise.resolve('')); + workspace.setup((x) => x.getConfiguration('python', TypeMoq.It.isAny())).returns(() => config.object); + pythonExecutionFactory + .setup((f) => f.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonExecutionService.object)); + fileSystem.setup((fs) => fs.getFileHash(TypeMoq.It.isAny())).returns(() => Promise.resolve('')); persistentStateFactory - .setup(p => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .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, experiments.object); serviceManager.addSingletonInstance(IDisposableRegistry, []); serviceManager.addSingletonInstance(IInterpreterHelper, helper.object); - serviceManager.addSingletonInstance(IPythonPathUpdaterServiceManager, updater.object); + serviceManager.addSingletonInstance( + IPythonPathUpdaterServiceManager, + updater.object, + ); serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); - serviceManager.addSingletonInstance(IInterpreterLocatorService, locator.object, INTERPRETER_LOCATOR_SERVICE); serviceManager.addSingletonInstance(IFileSystem, fileSystem.object); + serviceManager.addSingletonInstance( + IInterpreterPathService, + interpreterPathService.object, + ); serviceManager.addSingletonInstance(IInterpreterDisplay, interpreterDisplay.object); - serviceManager.addSingletonInstance(IVirtualEnvironmentManager, virtualEnvMgr.object); - serviceManager.addSingletonInstance(IPersistentStateFactory, persistentStateFactory.object); - serviceManager.addSingletonInstance(IPythonExecutionFactory, pythonExecutionFactory.object); - serviceManager.addSingletonInstance(IPythonExecutionService, pythonExecutionService.object); - serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); + serviceManager.addSingletonInstance( + IPersistentStateFactory, + persistentStateFactory.object, + ); + serviceManager.addSingletonInstance( + IPythonExecutionFactory, + pythonExecutionFactory.object, + ); + serviceManager.addSingletonInstance( + IPythonExecutionService, + pythonExecutionService.object, + ); + serviceManager.addSingleton( + IInterpreterAutoSelectionService, + MockAutoSelectionService, + ); + serviceManager.addSingleton( + IInterpreterAutoSelectionProxyService, + MockAutoSelectionService, + ); + installer.setup((i) => i.isInstalled(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + serviceManager.addSingletonInstance(IInstaller, installer.object); + serviceManager.addSingletonInstance(IApplicationShell, appShell.object); serviceManager.addSingletonInstance(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); - 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))) - .returns(() => Promise.resolve([])) - .verifiable(TypeMoq.Times.once()); - - const service = new InterpreterService(serviceContainer); - await service.getInterpreters(resource); - - locator.verifyAll(); - }); - }); - test('Changes to active document should invoke interpreter.refresh method', async () => { - const service = new InterpreterService(serviceContainer); - const documentManager = TypeMoq.Mock.ofType(); + reportActiveInterpreterChangedStub = sinon.stub(proposedApi, 'reportActiveInterpreterChanged'); + }); - 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(); - const uri = Uri.file(path.join('usr', 'file.py')); - const document = TypeMoq.Mock.ofType(); - 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()); + teardown(() => { + sinon.restore(); + }); + + [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, pyenvs.object); + await service.refresh(resource); + + interpreterDisplay.verifyAll(); }); + }); - test('If there is no active document then interpreter.refresh should not be invoked', async () => { - const service = new InterpreterService(serviceContainer); - const documentManager = TypeMoq.Mock.ofType(); + test('Changes to active document should invoke interpreter.refresh method', async () => { + const service = new InterpreterService(serviceContainer, pyenvs.object); + const documentManager = createTypeMoq(); - let activeTextEditorChangeHandler: Function | undefined; - documentManager.setup(d => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { + 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); + 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()); - }); + service.initialize(); + const textEditor = createTypeMoq(); + const uri = Uri.file(path.join('usr', 'file.py')); + const document = createTypeMoq(); + 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()); }); - 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); - locator - .setup(l => l.getInterpreters(TypeMoq.It.isValue(resource))) - .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('If there is no active document then interpreter.refresh should not be invoked', async () => { + const service = new InterpreterService(serviceContainer, pyenvs.object); + const documentManager = createTypeMoq(); - suite('Caching Display name', () => { - setup(() => { - setupSuite(); - fileSystem.reset(); - persistentStateFactory.reset(); - }); - test('Return cached display name', async () => { - const pythonPath = '1234'; - const interpreterInfo: Partial = { path: pythonPath }; - const fileHash = 'File_Hash'; - fileSystem - .setup(fs => fs.getFileHash(TypeMoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve(fileHash)) - .verifiable(TypeMoq.Times.once()); - 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, displayName: expectedDisplayName } - }; - return state as any; - }) - .verifiable(TypeMoq.Times.once()); + 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); - const displayName = await service.getDisplayName(interpreterInfo, undefined); + service.initialize(); + activeTextEditorChangeHandler!(); - expect(displayName).to.equal(expectedDisplayName); - fileSystem.verifyAll(); - persistentStateFactory.verifyAll(); - }); - test('Cached display name is not used if file hashes differ', async () => { - const pythonPath = '1234'; - const interpreterInfo: Partial = { path: pythonPath }; - const fileHash = 'File_Hash'; - fileSystem - .setup(fs => fs.getFileHash(TypeMoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve(fileHash)) - .verifiable(TypeMoq.Times.once()); - 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()); + interpreterDisplay.verify((i) => i.refresh(TypeMoq.It.isValue(undefined)), TypeMoq.Times.never()); + }); - const service = new InterpreterService(serviceContainer); - const displayName = await service.getDisplayName(interpreterInfo, undefined).catch(() => ''); + test('Register the correct handler', async () => { + const service = new InterpreterService(serviceContainer, pyenvs.object); + const documentManager = createTypeMoq(); + + 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(); + }); - expect(displayName).to.not.equal(expectedDisplayName); - fileSystem.verifyAll(); - persistentStateFactory.verifyAll(); + 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, }); }); - // 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) 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) 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 = { - 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); - const expectedDisplayName = buildDisplayName(interpreterInfo); - - const displayName = await service.getDisplayName(interpreterInfo, resource); - expect(displayName).to.equal(expectedDisplayName); - }); - - function buildDisplayName(interpreterInfo: Partial) { - 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('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('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 4e50ae221fcf..000000000000 --- a/src/test/interpreters/interpreterVersion.unit.test.ts +++ /dev/null @@ -1,67 +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'; - -suite('Interpreters display version', () => { - let processService: typeMoq.IMock; - let interpreterVersionService: IInterpreterVersionService; - - setup(() => { - const processFactory = typeMoq.Mock.ofType(); - processService = typeMoq.Mock.ofType(); - // 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); - }); - test('Must return the Python Version', async () => { - const pythonPath = path.join('a', 'b', 'python'); - const pythonVersion = 'Output from the Procecss'; - processService - .setup(p => p.exec(typeMoq.It.isValue(pythonPath), typeMoq.It.isValue(['--version']), typeMoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: pythonVersion })) - .verifiable(typeMoq.Times.once()); - - const pyVersion = await interpreterVersionService.getVersion(pythonPath, 'DEFAULT_TEST_VALUE'); - assert.equal(pyVersion, pythonVersion, 'Incorrect version'); - }); - test('Must return the default value when Python path is invalid', async () => { - const pythonPath = path.join('a', 'b', 'python'); - processService - .setup(p => p.exec(typeMoq.It.isValue(pythonPath), typeMoq.It.isValue(['--version']), typeMoq.It.isAny())) - .returns(() => Promise.reject({})) - .verifiable(typeMoq.Times.once()); - - const pyVersion = await interpreterVersionService.getVersion(pythonPath, 'DEFAULT_TEST_VALUE'); - assert.equal(pyVersion, 'DEFAULT_TEST_VALUE', 'Incorrect version'); - }); - 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(['-m', '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(['-m', '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/knownPathService.unit.test.ts b/src/test/interpreters/knownPathService.unit.test.ts deleted file mode 100644 index 1a2467790ef3..000000000000 --- a/src/test/interpreters/knownPathService.unit.test.ts +++ /dev/null @@ -1,105 +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 { KnownSearchPathsForInterpreters } from '../../client/interpreter/locators/services/KnownPathsService'; -import { IServiceContainer } from '../../client/ioc/types'; - -suite('Interpreters Known Paths', () => { - let serviceContainer: TypeMoq.IMock; - let currentProcess: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - let pathUtils: TypeMoq.IMock; - let knownSearchPaths: IKnownSearchPathsForInterpreters; - - setup(async () => { - serviceContainer = TypeMoq.Mock.ofType(); - currentProcess = TypeMoq.Mock.ofType(); - platformService = TypeMoq.Mock.ofType(); - pathUtils = TypeMoq.Mock.ofType(); - 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/interpreters/locators/cacheableLocatorService.unit.test.ts b/src/test/interpreters/locators/cacheableLocatorService.unit.test.ts deleted file mode 100644 index 1d0a210c26ca..000000000000 --- a/src/test/interpreters/locators/cacheableLocatorService.unit.test.ts +++ /dev/null @@ -1,236 +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 { noop } from '../../../client/common/utils/misc'; -import { IInterpreterWatcher, PythonInterpreter } from '../../../client/interpreter/contracts'; -import { CacheableLocatorService } from '../../../client/interpreter/locators/services/cacheableLocatorService'; -import { ServiceContainer } from '../../../client/ioc/container'; - -suite('Interpreters - Cacheable Locator Service', () => { - suite('Caching', () => { - class Locator extends CacheableLocatorService { - constructor(name, serviceCcontainer, private readonly mockLocator: MockLocator) { - super(name, serviceCcontainer); - } - public dispose() { - noop(); - } - protected async getInterpretersImplementation(resource?: Uri): Promise { - 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 { - 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: Uri | undefined): Promise { - 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: void) => any, thisArgs?: any, disposables?: Disposable[]): Disposable { - return { dispose: noop }; - } - } - const watcher: IInterpreterWatcher = mock(Watcher); - - const locator = new class extends Locator { - protected async getInterpreterWatchers(_resource: Uri | undefined): Promise { - 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: void) => any; - public onDidCreate(listener: (e: void) => 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: Uri | undefined): Promise { - 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: Uri | undefined): Promise { - 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 { - 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)).thenReturn(instance(workspace)); - when(serviceContainer.get(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)).thenReturn(instance(workspace)); - when(serviceContainer.get(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/interpreters/locators/helpers.unit.test.ts b/src/test/interpreters/locators/helpers.unit.test.ts deleted file mode 100644 index f94d8b93752b..000000000000 --- a/src/test/interpreters/locators/helpers.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:max-func-body-length - -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 { getNamesAndValues } from '../../../client/common/utils/enum'; -import { Architecture } from '../../../client/common/utils/platform'; -import { IInterpreterHelper, IInterpreterLocatorHelper, InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts'; -import { InterpreterLocatorHelper } from '../../../client/interpreter/locators/helpers'; -import { IServiceContainer } from '../../../client/ioc/types'; - -enum OS { - Windows = 'Windows', - Linux = 'Linux', - Mac = 'Mac' -} - -suite('Interpreters - Locators Helper', () => { - let serviceContainer: TypeMoq.IMock; - let platform: TypeMoq.IMock; - let helper: IInterpreterLocatorHelper; - let fs: TypeMoq.IMock; - let interpreterServiceHelper: TypeMoq.IMock; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - platform = TypeMoq.Mock.ofType(); - fs = TypeMoq.Mock.ofType(); - interpreterServiceHelper = TypeMoq.Mock.ofType(); - 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(serviceContainer.object); - }); - 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); - - const items = helper.mergeInterpreters(interpreters); - - interpreterServiceHelper.verifyAll(); - platform.verifyAll(); - fs.verifyAll(); - expect(items).to.be.lengthOf(4); - expect(items).to.be.deep.equal(expectedInterpreters); - }); - getNamesAndValues(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); - } - }); - - const items = 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).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); - } - }); - - const items = 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/interpreters/locators/index.unit.test.ts b/src/test/interpreters/locators/index.unit.test.ts deleted file mode 100644 index fc4fa6c7c6f5..000000000000 --- a/src/test/interpreters/locators/index.unit.test.ts +++ /dev/null @@ -1,166 +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, InterpreterType, KNOWN_PATH_SERVICE, PIPENV_SERVICE, PythonInterpreter, WINDOWS_REGISTRY_SERVICE, WORKSPACE_VIRTUAL_ENV_SERVICE } from '../../../client/interpreter/contracts'; -import { PythonInterpreterLocatorService } from '../../../client/interpreter/locators'; -import { IServiceContainer } from '../../../client/ioc/types'; - -suite('Interpreters - Locators Index', () => { - let serviceContainer: TypeMoq.IMock; - let platformSvc: TypeMoq.IMock; - let helper: TypeMoq.IMock; - let locator: IInterpreterLocatorService; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - platformSvc = TypeMoq.Mock.ofType(); - helper = TypeMoq.Mock.ofType(); - 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).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(); - 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(() => 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(); - 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(() => expectedInterpreters) - .verifiable(TypeMoq.Times.once()); - - const interpreters = await locator.getInterpreters(resource); - - locatorsWithInterpreters.forEach(item => item.locator.verifyAll()); - helper.verifyAll(); - expect(interpreters).to.be.lengthOf(locatorsTypes.length); - expect(interpreters).to.be.deep.equal(expectedInterpreters); - }); - }); - }); -}); diff --git a/src/test/interpreters/locators/interpreterWatcherBuilder.unit.test.ts b/src/test/interpreters/locators/interpreterWatcherBuilder.unit.test.ts deleted file mode 100644 index b5e9505a6da0..000000000000 --- a/src/test/interpreters/locators/interpreterWatcherBuilder.unit.test.ts +++ /dev/null @@ -1,48 +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 { IInterpreterWatcher, WORKSPACE_VIRTUAL_ENV_SERVICE } from '../../../client/interpreter/contracts'; -import { InterpreterWatcherBuilder } from '../../../client/interpreter/locators/services/interpreterWatcherBuilder'; -import { ServiceContainer } from '../../../client/ioc/container'; - -suite('Interpreters - Watcher Builder', () => { - test('Build Workspace Virtual Env Watcher', async () => { - const workspaceService = mock(WorkspaceService); - const serviceContainer = mock(ServiceContainer); - const builder = new InterpreterWatcherBuilder(instance(workspaceService), instance(serviceContainer)); - const watcher = { register: () => Promise.resolve() }; - - when(workspaceService.getWorkspaceFolder(anything())).thenReturn(); - when(serviceContainer.get(IInterpreterWatcher, WORKSPACE_VIRTUAL_ENV_SERVICE)).thenReturn(watcher as any as IInterpreterWatcher); - - const item = await builder.getWorkspaceVirtualEnvInterpreterWatcher(undefined); - - expect(item).to.be.equal(watcher, 'invalid'); - }); - test('Ensure we cache Workspace Virtual Env Watcher', async () => { - const workspaceService = mock(WorkspaceService); - const serviceContainer = mock(ServiceContainer); - const builder = new InterpreterWatcherBuilder(instance(workspaceService), instance(serviceContainer)); - const watcher = { register: () => Promise.resolve() }; - - when(workspaceService.getWorkspaceFolder(anything())).thenReturn(); - when(serviceContainer.get(IInterpreterWatcher, WORKSPACE_VIRTUAL_ENV_SERVICE)).thenReturn(watcher as any as IInterpreterWatcher); - - const [item1, item2, item3] = await Promise.all([ - builder.getWorkspaceVirtualEnvInterpreterWatcher(undefined), - builder.getWorkspaceVirtualEnvInterpreterWatcher(undefined), - builder.getWorkspaceVirtualEnvInterpreterWatcher(undefined) - ]); - - expect(item1).to.be.equal(watcher, 'invalid'); - expect(item2).to.be.equal(watcher, 'invalid'); - expect(item3).to.be.equal(watcher, 'invalid'); - }); -}); diff --git a/src/test/interpreters/locators/progressService.unit.test.ts b/src/test/interpreters/locators/progressService.unit.test.ts deleted file mode 100644 index 75c719c6c000..000000000000 --- a/src/test/interpreters/locators/progressService.unit.test.ts +++ /dev/null @@ -1,112 +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, PythonInterpreter } from '../../../client/interpreter/contracts'; -import { InterpreterLocatorProgressService } from '../../../client/interpreter/locators/progressService'; -import { ServiceContainer } from '../../../client/ioc/container'; -import { sleep } from '../../core'; - -suite('Interpreters - Locator Progress', () => { - class Locator implements IInterpreterLocatorService { - public get hasInterpreters(): Promise { - return Promise.resolve(true); - } - public locatingCallback?: (e: Promise) => any; - public onLocating(listener: (e: Promise) => any, thisArgs?: any, disposables?: Disposable[]): Disposable { - this.locatingCallback = listener; - return { dispose: noop }; - } - public getInterpreters(resource?: Uri): Promise { - 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(); - 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(); - 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(); - locator1.locatingCallback!.bind(progress)(locatingDeferred1.promise); - - const locatingDeferred2 = createDeferred(); - locator2.locatingCallback!.bind(progress)(locatingDeferred2.promise); - - const locatingDeferred3 = createDeferred(); - 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/interpreters/locators/workspaceVirtualEnvService.test.ts b/src/test/interpreters/locators/workspaceVirtualEnvService.test.ts deleted file mode 100644 index e2641ef39dd6..000000000000 --- a/src/test/interpreters/locators/workspaceVirtualEnvService.test.ts +++ /dev/null @@ -1,109 +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 { Uri } from 'vscode'; -import '../../../client/common/extensions'; -import { - IInterpreterLocatorService, WORKSPACE_VIRTUAL_ENV_SERVICE -} from '../../../client/interpreter/contracts'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { - deleteFiles, isOs, isPythonVersionInProcess, OSType, - PYTHON_PATH, rootWorkspaceUri, waitForCondition -} from '../../common'; -import { IS_MULTI_ROOT_TEST } from '../../constants'; -import { initialize, IS_VSTS, multirootPath } from '../../initialize'; - -const timeoutMs = 60_000; -suite('Interpreters - Workspace VirtualEnv Service', function () { - this.timeout(timeoutMs); - this.retries(0); - - let locator: IInterpreterLocatorService; - const workspaceUri = IS_MULTI_ROOT_TEST ? Uri.file(path.join(multirootPath, 'workspace3')) : rootWorkspaceUri!; - const workspace4 = Uri.file(path.join(multirootPath, 'workspace4')); - const venvPrefix = '.venv'; - let serviceContainer: IServiceContainer; - - async function waitForInterpreterToBeDetected(envNameToLookFor: string) { - const predicate = async () => { - const items = await locator.getInterpreters(workspaceUri); - return items.some(item => item.envName === envNameToLookFor); - }; - await waitForCondition(predicate, timeoutMs, `${envNameToLookFor}, Environment not detected in the workspace ${workspaceUri.fsPath}`); - } - async function createVirtualEnvironment(envSuffix: string) { - // Ensure env is random to avoid conflicts in tests (currupting test data). - const envName = `${venvPrefix}${envSuffix}${new Date().getTime().toString()}`; - return new Promise((resolve, reject) => { - exec(`${PYTHON_PATH.fileToCommandArgument()} -m venv ${envName}`, { cwd: workspaceUri.fsPath }, (ex, _, stderr) => { - if (ex) { - return reject(ex); - } - if (stderr && stderr.length > 0) { - reject(new Error(`Failed to create Env ${envName}, ${PYTHON_PATH}, Error: ${stderr}`)); - } else { - resolve(envName); - } - }); - }); - } - - suiteSetup(async function () { - // skip for Linux CI, see #3848 - if (isOs(OSType.Linux) && IS_VSTS) { - return this.skip(); - } - - // skip for Python < 3, no venv support - if (await isPythonVersionInProcess(undefined, '2')) { - return this.skip(); - } - - serviceContainer = (await initialize()).serviceContainer; - locator = serviceContainer.get(IInterpreterLocatorService, WORKSPACE_VIRTUAL_ENV_SERVICE); - // This test is required, we need to wait for interpreter listing completes, - // before proceeding with other tests. - await deleteFiles(path.join(workspaceUri.fsPath, `${venvPrefix}*`)); - await locator.getInterpreters(workspaceUri); - }); - - suiteTeardown(async () => deleteFiles(path.join(workspaceUri.fsPath, `${venvPrefix}*`))); - teardown(async () => deleteFiles(path.join(workspaceUri.fsPath, `${venvPrefix}*`))); - - 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/interpreters/locators/workspaceVirtualEnvService.unit.test.ts b/src/test/interpreters/locators/workspaceVirtualEnvService.unit.test.ts deleted file mode 100644 index 54c2c597cc4e..000000000000 --- a/src/test/interpreters/locators/workspaceVirtualEnvService.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 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 { InterpreterWatcherBuilder } from '../../../client/interpreter/locators/services/interpreterWatcherBuilder'; -import { WorkspaceVirtualEnvService } from '../../../client/interpreter/locators/services/workspaceVirtualEnvService'; -import { ServiceContainer } from '../../../client/ioc/container'; - -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 { - 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/interpreters/locators/workspaceVirtualEnvWatcherService.unit.test.ts b/src/test/interpreters/locators/workspaceVirtualEnvWatcherService.unit.test.ts deleted file mode 100644 index 026704ad4415..000000000000 --- a/src/test/interpreters/locators/workspaceVirtualEnvWatcherService.unit.test.ts +++ /dev/null @@ -1,124 +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/interpreter/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/interpreters/mocks.ts b/src/test/interpreters/mocks.ts index c69c960ac325..12401115eb36 100644 --- a/src/test/interpreters/mocks.ts +++ b/src/test/interpreters/mocks.ts @@ -2,15 +2,16 @@ 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 }[]) { - } + constructor( + private keys: { key: string; hive: RegistryHive; arch?: Architecture; values: string[] }[], + private values: { key: string; hive: RegistryHive; arch?: Architecture; value: string; name?: string }[], + ) {} public async getKeys(key: string, hive: RegistryHive, arch?: Architecture): Promise { - const items = this.keys.find(item => { + const items = this.keys.find((item) => { if (typeof item.arch === 'number') { return item.key === key && item.hive === hive && item.arch === arch; } @@ -19,8 +20,13 @@ export class MockRegistry implements IRegistry { return items ? Promise.resolve(items.values) : Promise.resolve([]); } - public async getValue(key: string, hive: RegistryHive, arch?: Architecture, name?: string): Promise { - const items = this.values.find(item => { + public async getValue( + key: string, + hive: RegistryHive, + arch?: Architecture, + name?: string, + ): Promise { + const items = this.values.find((item) => { if (item.key !== key || item.hive !== hive) { return false; } @@ -37,31 +43,16 @@ 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) { } - public async getVersion(pythonPath: string, defaultDisplayName: string): Promise { - return this.useDefaultDisplayName ? Promise.resolve(defaultDisplayName) : Promise.resolve(this.displayName); - } - public async getPipVersion(pythonPath: string): Promise { - // 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 { - // tslint:disable-next-line:no-any - constructor(public data: any) { } - // tslint:disable-next-line:no-any + constructor(public data: any) {} + + public readonly storage = new MockMemento(); + get value(): any { return this.data; } - public async updateValue(data): Promise { + + public async updateValue(data: any): Promise { this.data = data; } } diff --git a/src/test/interpreters/pipEnvService.unit.test.ts b/src/test/interpreters/pipEnvService.unit.test.ts deleted file mode 100644 index 84d9069e05e5..000000000000 --- a/src/test/interpreters/pipEnvService.unit.test.ts +++ /dev/null @@ -1,161 +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 { 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 { ICurrentProcess, ILogger, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; -import { getNamesAndValues } from '../../client/common/utils/enum'; -import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; -import { IInterpreterHelper, IInterpreterLocatorService } from '../../client/interpreter/contracts'; -import { PipEnvService } from '../../client/interpreter/locators/services/pipEnvService'; -import { IServiceContainer } from '../../client/ioc/types'; - -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: IInterpreterLocatorService; - let serviceContainer: TypeMoq.IMock; - let interpreterHelper: TypeMoq.IMock; - let processService: TypeMoq.IMock; - let currentProcess: TypeMoq.IMock; - let fileSystem: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let persistentStateFactory: TypeMoq.IMock; - let envVarsProvider: TypeMoq.IMock; - let procServiceFactory: TypeMoq.IMock; - let logger: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - const workspaceService = TypeMoq.Mock.ofType(); - interpreterHelper = TypeMoq.Mock.ofType(); - fileSystem = TypeMoq.Mock.ofType(); - processService = TypeMoq.Mock.ofType(); - appShell = TypeMoq.Mock.ofType(); - currentProcess = TypeMoq.Mock.ofType(); - persistentStateFactory = TypeMoq.Mock.ofType(); - envVarsProvider = TypeMoq.Mock.ofType(); - procServiceFactory = TypeMoq.Mock.ofType(); - logger = TypeMoq.Mock.ofType(); - platformService = TypeMoq.Mock.ofType(); - 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>(); - 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.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(ILogger))).returns(() => logger.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); - - pipEnvService = new PipEnvService(serviceContainer.object); - }); - - 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 --venv\' failes ${testSuffix}`, async () => { - const env = {}; - currentProcess.setup(c => c.env).returns(() => env); - processService.setup(p => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.reject('')); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))).returns(() => Promise.resolve(true)); - appShell.setup(a => a.showWarningMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('')).verifiable(TypeMoq.Times.once()); - logger.setup(l => l.logWarning(TypeMoq.It.isAny(), TypeMoq.It.isAny())).verifiable(TypeMoq.Times.exactly(2)); - const environments = await pipEnvService.getInterpreters(resource); - - expect(environments).to.be.deep.equal([]); - appShell.verifyAll(); - logger.verifyAll(); - }); - test(`Should display warning message if there is a \'PipFile\' but \'pipenv --venv\' failes with stderr ${testSuffix}`, async () => { - const env = {}; - 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({ stderr: 'PipEnv Failed', stdout: '' })); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))).returns(() => Promise.resolve(true)); - appShell.setup(a => a.showWarningMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('')).verifiable(TypeMoq.Times.once()); - logger.setup(l => l.logWarning(TypeMoq.It.isAny(), TypeMoq.It.isAny())).verifiable(TypeMoq.Times.exactly(2)); - const environments = await pipEnvService.getInterpreters(resource); - - expect(environments).to.be.deep.equal([]); - appShell.verifyAll(); - logger.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(); - }); - }); - }); -}); diff --git a/src/test/interpreters/pythonPathUpdater.test.ts b/src/test/interpreters/pythonPathUpdater.test.ts deleted file mode 100644 index 35657bf82703..000000000000 --- a/src/test/interpreters/pythonPathUpdater.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/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; - let workspaceService: TypeMoq.IMock; - let updaterServiceFactory: IPythonPathUpdaterServiceFactory; - function setupMocks() { - serviceContainer = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - updaterServiceFactory = new PythonPathUpdaterServiceFactory(serviceContainer.object); - } - function setupConfigProvider(resource?: Uri): TypeMoq.IMock { - const workspaceConfig = TypeMoq.Mock.ofType(); - workspaceService.setup(w => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))).returns(() => workspaceConfig.object); - return workspaceConfig; - } - suite('Global', () => { - setup(setupMocks); - 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); - test('Python Path should not be updated when current pythonPath is the same', async () => { - const workspaceFolderPath = path.join('user', 'desktop', 'development'); - const workspaceFolder = Uri.file(workspaceFolderPath); - const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); - const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; - const workspaceConfig = setupConfigProvider(workspaceFolder); - workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => { - // tslint:disable-next-line:no-any - return { workspaceFolderValue: pythonPath } as any; - }); - - await updater.updatePythonPath(pythonPath); - workspaceConfig.verify(w => w.update(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - }); - test('Python Path should be updated when current pythonPath is different', async () => { - const workspaceFolderPath = path.join('user', 'desktop', 'development'); - const workspaceFolder = Uri.file(workspaceFolderPath); - const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); - const pythonPath = `xWorkspaceFolderPythonPath${new Date().getMilliseconds()}`; - const workspaceConfig = setupConfigProvider(workspaceFolder); - workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); - - await updater.updatePythonPath(pythonPath); - workspaceConfig.verify(w => w.update(TypeMoq.It.isValue('pythonPath'), TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder)), TypeMoq.Times.once()); - }); - test('Python Path should be truncated for worspace-relative paths', async () => { - const workspaceFolderPath = path.join('user', 'desktop', 'development'); - const workspaceFolder = Uri.file(workspaceFolderPath); - const updater = updaterServiceFactory.getWorkspaceFolderPythonPathConfigurationService(workspaceFolder); - const pythonPath = Uri.file(path.join(workspaceFolderPath, 'env', 'bin', 'python')).fsPath; - const expectedPythonPath = path.join('env', 'bin', 'python'); - const workspaceConfig = setupConfigProvider(workspaceFolder); - workspaceConfig.setup(w => w.inspect(TypeMoq.It.isValue('pythonPath'))).returns(() => undefined); - - await updater.updatePythonPath(pythonPath); - workspaceConfig.verify(w => w.update(TypeMoq.It.isValue('pythonPath'), TypeMoq.It.isValue(expectedPythonPath), TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder)), TypeMoq.Times.once()); - }); - }); - suite('Workspace (multiroot scenario)', () => { - setup(setupMocks); - 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()); - }); - }); -}); diff --git a/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts new file mode 100644 index 000000000000..5c851b8071f3 --- /dev/null +++ b/src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts @@ -0,0 +1,134 @@ +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/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'; + +suite('Python Path Settings Updater', () => { + let serviceContainer: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + let experimentsManager: TypeMoq.IMock; + let interpreterPathService: TypeMoq.IMock; + let updaterServiceFactory: IPythonPathUpdaterServiceFactory; + function setupMocks() { + serviceContainer = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + interpreterPathService = TypeMoq.Mock.ofType(); + experimentsManager = TypeMoq.Mock.ofType(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .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); + } + + 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(); + }); + 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(); + }); + 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()); + 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(); + }); + }); +}); diff --git a/src/test/interpreters/recommededEnvironmentService.unit.test.ts b/src/test/interpreters/recommededEnvironmentService.unit.test.ts new file mode 100644 index 000000000000..7cb5aed58239 --- /dev/null +++ b/src/test/interpreters/recommededEnvironmentService.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 { anything, reset, verify, when } from 'ts-mockito'; +import { Disposable, Uri } from 'vscode'; +import { mockedVSCodeNamespaces } from '../vscode-mock'; +import { RecommendedEnvironmentService } from '../../client/interpreter/configuration/recommededEnvironmentService'; + +suite('RecommendedEnvironmentService - activate', () => { + let service: RecommendedEnvironmentService; + let subscriptions: Disposable[]; + + setup(() => { + subscriptions = []; + const extensionContext = { + subscriptions, + globalState: { + get: () => undefined, + update: () => Promise.resolve(), + }, + } as any; + + when(mockedVSCodeNamespaces.commands!.registerCommand(anything(), anything())).thenReturn({ + dispose: () => {}, + } as Disposable); + + service = new RecommendedEnvironmentService(extensionContext); + }); + + teardown(() => { + reset(mockedVSCodeNamespaces.commands!); + }); + + test('Multiroot workspace: command is registered only once across multiple activate calls', async () => { + // Simulate multiroot workspace where activate is called once per workspace root + const workspaceRoot1 = Uri.file('/workspace/root1'); + const workspaceRoot2 = Uri.file('/workspace/root2'); + const workspaceRoot3 = Uri.file('/workspace/root3'); + + await service.activate(workspaceRoot1); + await service.activate(workspaceRoot2); + await service.activate(workspaceRoot3); + + verify(mockedVSCodeNamespaces.commands!.registerCommand('python.getRecommendedEnvironment', anything())).once(); + expect(subscriptions).to.have.lengthOf(1); + }); +}); diff --git a/src/test/interpreters/serviceRegistry.unit.test.ts b/src/test/interpreters/serviceRegistry.unit.test.ts new file mode 100644 index 000000000000..ad8614b42d8b --- /dev/null +++ b/src/test/interpreters/serviceRegistry.unit.test.ts @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { IExtensionActivationService, IExtensionSingleActivationService } from '../../client/activation/types'; +import { EnvironmentActivationService } from '../../client/interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; +import { InterpreterAutoSelectionService } from '../../client/interpreter/autoSelection'; +import { InterpreterAutoSelectionProxyService } from '../../client/interpreter/autoSelection/proxy'; +import { + IInterpreterAutoSelectionService, + IInterpreterAutoSelectionProxyService, +} from '../../client/interpreter/autoSelection/types'; +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 { 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, + IRecommendedEnvironmentService, +} from '../../client/interpreter/configuration/types'; +import { + IActivatedEnvironmentLaunch, + IInterpreterDisplay, + IInterpreterHelper, + IInterpreterService, +} from '../../client/interpreter/contracts'; +import { InterpreterDisplay } from '../../client/interpreter/display'; +import { InterpreterLocatorProgressStatusBarHandler } from '../../client/interpreter/display/progressDisplay'; +import { InterpreterHelper } from '../../client/interpreter/helpers'; +import { InterpreterService } from '../../client/interpreter/interpreterService'; +import { registerTypes } from '../../client/interpreter/serviceRegistry'; +import { ActivatedEnvironmentLaunch } from '../../client/interpreter/virtualEnvs/activatedEnvLaunch'; +import { CondaInheritEnvPrompt } from '../../client/interpreter/virtualEnvs/condaInheritEnvPrompt'; +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', () => { + const serviceManager = mock(ServiceManager); + registerTypes(instance(serviceManager)); + + [ + [IExtensionSingleActivationService, InstallPythonCommand], + [IExtensionSingleActivationService, InstallPythonViaTerminal], + [IExtensionSingleActivationService, SetInterpreterCommand], + [IInterpreterQuickPick, SetInterpreterCommand], + [IExtensionSingleActivationService, ResetInterpreterCommand], + + [IExtensionActivationService, VirtualEnvironmentPrompt], + + [IInterpreterService, InterpreterService], + [IInterpreterDisplay, InterpreterDisplay], + + [IPythonPathUpdaterServiceFactory, PythonPathUpdaterServiceFactory], + [IPythonPathUpdaterServiceManager, PythonPathUpdaterService], + [IRecommendedEnvironmentService, RecommendedEnvironmentService], + [IInterpreterSelector, InterpreterSelector], + [IInterpreterHelper, InterpreterHelper], + [IInterpreterComparer, EnvironmentTypeComparer], + + [IExtensionSingleActivationService, InterpreterLocatorProgressStatusBarHandler], + + [IInterpreterAutoSelectionProxyService, InterpreterAutoSelectionProxyService], + [IInterpreterAutoSelectionService, InterpreterAutoSelectionService], + + [EnvironmentActivationService, EnvironmentActivationService], + [IEnvironmentActivationService, EnvironmentActivationService], + [IExtensionSingleActivationService, InterpreterPathCommand], + [IExtensionActivationService, CondaInheritEnvPrompt], + [IActivatedEnvironmentLaunch, ActivatedEnvironmentLaunch], + ].forEach((mapping) => { + // eslint-disable-next-line prefer-spread + verify(serviceManager.addSingleton.apply(serviceManager, mapping as never)).once(); + }); + }); +}); diff --git a/src/test/interpreters/venv.unit.test.ts b/src/test/interpreters/venv.unit.test.ts deleted file mode 100644 index 8ab27cbb71ca..000000000000 --- a/src/test/interpreters/venv.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 { 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 { GlobalVirtualEnvironmentsSearchPathProvider } from '../../client/interpreter/locators/services/globalVirtualEnvService'; -import { WorkspaceVirtualEnvironmentsSearchPathProvider } from '../../client/interpreter/locators/services/workspaceVirtualEnvService'; -import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; - -suite('Virtual environments', () => { - let serviceManager: ServiceManager; - let serviceContainer: ServiceContainer; - let settings: TypeMoq.IMock; - let config: TypeMoq.IMock; - let workspace: TypeMoq.IMock; - let process: TypeMoq.IMock; - let virtualEnvMgr: TypeMoq.IMock; - - setup(() => { - const cont = new Container(); - serviceManager = new ServiceManager(cont); - serviceContainer = new ServiceContainer(cont); - - settings = TypeMoq.Mock.ofType(); - config = TypeMoq.Mock.ofType(); - workspace = TypeMoq.Mock.ofType(); - process = TypeMoq.Mock.ofType(); - virtualEnvMgr = TypeMoq.Mock.ofType(); - - config.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - - serviceManager.addSingletonInstance(IConfigurationService, config.object); - serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); - serviceManager.addSingletonInstance(ICurrentProcess, process.object); - serviceManager.addSingletonInstance(IVirtualEnvironmentManager, virtualEnvMgr.object); - serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); - }); - - test('Global search paths', async () => { - const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); - - const homedir = os.homedir(); - const folders = ['Envs', '.virtualenvs']; - 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 = 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('Workspace search paths', async () => { - settings.setup(x => x.venvPath).returns(() => path.join('~', 'foo')); - - const wsRoot = TypeMoq.Mock.ofType(); - wsRoot.setup(x => x.uri).returns(() => Uri.file('root')); - - const folder1 = TypeMoq.Mock.ofType(); - 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/interpreters/virtualEnvManager.unit.test.ts b/src/test/interpreters/virtualEnvManager.unit.test.ts deleted file mode 100644 index 5ac6741d9b13..000000000000 --- a/src/test/interpreters/virtualEnvManager.unit.test.ts +++ /dev/null @@ -1,83 +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(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IPipEnvService))).returns(() => TypeMoq.Mock.ofType().object); - const workspaceService = TypeMoq.Mock.ofType(); - 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 workspacec name as env name', async () => { - const serviceContainer = TypeMoq.Mock.ofType(); - const pipEnvService = TypeMoq.Mock.ofType(); - 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().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().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(); - 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(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IProcessServiceFactory))).returns(() => TypeMoq.Mock.ofType().object); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IFileSystem))).returns(() => TypeMoq.Mock.ofType().object); - const pipEnvService = TypeMoq.Mock.ofType(); - 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(); - workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); - if (resource) { - const workspaceFolder = TypeMoq.Mock.ofType(); - 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; + let appShell: TypeMoq.IMock; + let pythonPathUpdaterService: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let processServiceFactory: TypeMoq.IMock; + let processService: TypeMoq.IMock; + 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(); + pythonPathUpdaterService = TypeMoq.Mock.ofType(); + appShell = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + processServiceFactory = TypeMoq.Mock.ofType(); + _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(); + pythonPathUpdaterService = TypeMoq.Mock.ofType(); + appShell = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + processServiceFactory = TypeMoq.Mock.ofType(); + processService = TypeMoq.Mock.ofType(); + 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 new file mode 100644 index 000000000000..9499b5294d78 --- /dev/null +++ b/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts @@ -0,0 +1,491 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as sinon from 'sinon'; +import { instance, mock, verify, when } from 'ts-mockito'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri, WorkspaceConfiguration } from 'vscode'; +import { + IApplicationEnvironment, + IApplicationShell, + IWorkspaceService, +} from '../../../client/common/application/types'; +import { PersistentStateFactory } from '../../../client/common/persistentState'; +import { IPlatformService } from '../../../client/common/platform/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, +} from '../../../client/interpreter/virtualEnvs/condaInheritEnvPrompt'; +import { EnvironmentType } from '../../../client/pythonEnvironments/info'; + +suite('Conda Inherit Env Prompt', async () => { + const resource = Uri.file('a'); + let workspaceService: TypeMoq.IMock; + let appShell: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let platformService: TypeMoq.IMock; + let applicationEnvironment: TypeMoq.IMock; + let persistentStateFactory: IPersistentStateFactory; + let notificationPromptEnabled: TypeMoq.IMock>; + let condaInheritEnvPrompt: CondaInheritEnvPrompt; + function verifyAll() { + workspaceService.verifyAll(); + appShell.verifyAll(); + interpreterService.verifyAll(); + } + + suite('Method shouldShowPrompt()', () => { + setup(() => { + workspaceService = TypeMoq.Mock.ofType(); + appShell = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + persistentStateFactory = mock(PersistentStateFactory); + platformService = TypeMoq.Mock.ofType(); + applicationEnvironment = TypeMoq.Mock.ofType(); + applicationEnvironment.setup((a) => a.remoteName).returns(() => undefined); + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + appShell.object, + instance(persistentStateFactory), + 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, + appShell.object, + instance(persistentStateFactory), + platformService.object, + applicationEnvironment.object, + true, + ); + const workspaceConfig = TypeMoq.Mock.ofType(); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(undefined) as any) + .verifiable(TypeMoq.Times.never()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.never()); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(true, 'Should be true'); + verifyAll(); + }); + test('Returns false if 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) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(false, 'Should be false'); + verifyAll(); + }); + test('Returns false if active interpreter is not of type Conda', async () => { + const interpreter = { + envType: EnvironmentType.Pipenv, + }; + const workspaceConfig = TypeMoq.Mock.ofType(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(interpreter) as any) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.never()); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(false, 'Should be false'); + verifyAll(); + }); + test('Returns false if no active interpreter is present', async () => { + const workspaceConfig = TypeMoq.Mock.ofType(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.never()); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(false, 'Should be false'); + verifyAll(); + }); + test('Returns false if settings returned is `undefined`', async () => { + const interpreter = { + envType: EnvironmentType.Conda, + }; + const workspaceConfig = TypeMoq.Mock.ofType(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(interpreter) as any) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.once()); + workspaceConfig + .setup((ws) => ws.inspect('integrated.inheritEnv')) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(false, 'Should be false'); + verifyAll(); + }); + [ + { + name: 'Returns false if globalValue `terminal.integrated.inheritEnv` setting is set', + settings: { + globalValue: true, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }, + }, + { + name: 'Returns false if workspaceValue of `terminal.integrated.inheritEnv` setting is set', + settings: { + globalValue: undefined, + workspaceValue: true, + workspaceFolderValue: undefined, + }, + }, + { + name: 'Returns false if workspaceFolderValue of `terminal.integrated.inheritEnv` setting is set', + settings: { + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: false, + }, + }, + ].forEach((testParams) => { + test(testParams.name, async () => { + const interpreter = { + envType: EnvironmentType.Conda, + }; + const workspaceConfig = TypeMoq.Mock.ofType(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(interpreter) as any) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.once()); + workspaceConfig + .setup((ws) => ws.inspect('integrated.inheritEnv')) + .returns(() => testParams.settings as any); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(false, 'Should be false'); + verifyAll(); + }); + }); + test('Returns true otherwise', async () => { + const interpreter = { + envType: EnvironmentType.Conda, + }; + const settings = { + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }; + const workspaceConfig = TypeMoq.Mock.ofType(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(interpreter) as any) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.once()); + workspaceConfig.setup((ws) => ws.inspect('integrated.inheritEnv')).returns(() => settings as any); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(true, 'Prompt should be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(true, 'Should be true'); + verifyAll(); + }); + }); + suite('Method activate()', () => { + let initializeInBackground: sinon.SinonStub; + setup(() => { + workspaceService = TypeMoq.Mock.ofType(); + appShell = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + persistentStateFactory = mock(PersistentStateFactory); + platformService = TypeMoq.Mock.ofType(); + applicationEnvironment = TypeMoq.Mock.ofType(); + applicationEnvironment.setup((a) => a.remoteName).returns(() => undefined); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Invokes initializeInBackground() in the background', async () => { + const initializeInBackgroundDeferred = createDeferred(); + initializeInBackground = sinon.stub(CondaInheritEnvPrompt.prototype, 'initializeInBackground'); + initializeInBackground.callsFake(() => initializeInBackgroundDeferred.promise); + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + appShell.object, + instance(persistentStateFactory), + + platformService.object, + applicationEnvironment.object, + ); + + const promise = condaInheritEnvPrompt.activate(resource); + const deferred = createDeferredFromPromise(promise); + await sleep(1); + + // Ensure activate() function has completed while initializeInBackground() is still not resolved + assert.strictEqual(deferred.completed, true); + + initializeInBackgroundDeferred.resolve(); + await sleep(1); + assert.ok(initializeInBackground.calledOnce); + }); + + test('Ignores errors raised by initializeInBackground()', async () => { + initializeInBackground = sinon.stub(CondaInheritEnvPrompt.prototype, 'initializeInBackground'); + initializeInBackground.rejects(new Error('Kaboom')); + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + appShell.object, + instance(persistentStateFactory), + + platformService.object, + applicationEnvironment.object, + ); + await condaInheritEnvPrompt.activate(resource); + assert.ok(initializeInBackground.calledOnce); + }); + }); + + suite('Method initializeInBackground()', () => { + let shouldShowPrompt: sinon.SinonStub; + let promptAndUpdate: sinon.SinonStub; + setup(() => { + workspaceService = TypeMoq.Mock.ofType(); + appShell = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + persistentStateFactory = mock(PersistentStateFactory); + platformService = TypeMoq.Mock.ofType(); + applicationEnvironment = TypeMoq.Mock.ofType(); + applicationEnvironment.setup((a) => a.remoteName).returns(() => undefined); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Show prompt if shouldShowPrompt() returns true', async () => { + shouldShowPrompt = sinon.stub(CondaInheritEnvPrompt.prototype, 'shouldShowPrompt'); + shouldShowPrompt.callsFake(() => Promise.resolve(true)); + promptAndUpdate = sinon.stub(CondaInheritEnvPrompt.prototype, 'promptAndUpdate'); + promptAndUpdate.callsFake(() => Promise.resolve(undefined)); + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + appShell.object, + instance(persistentStateFactory), + + platformService.object, + applicationEnvironment.object, + ); + await condaInheritEnvPrompt.initializeInBackground(resource); + assert.ok(shouldShowPrompt.calledOnce); + assert.ok(promptAndUpdate.calledOnce); + }); + + test('Do not show prompt if shouldShowPrompt() returns false', async () => { + shouldShowPrompt = sinon.stub(CondaInheritEnvPrompt.prototype, 'shouldShowPrompt'); + shouldShowPrompt.callsFake(() => Promise.resolve(false)); + promptAndUpdate = sinon.stub(CondaInheritEnvPrompt.prototype, 'promptAndUpdate'); + promptAndUpdate.callsFake(() => Promise.resolve(undefined)); + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + appShell.object, + instance(persistentStateFactory), + + platformService.object, + applicationEnvironment.object, + ); + await condaInheritEnvPrompt.initializeInBackground(resource); + assert.ok(shouldShowPrompt.calledOnce); + assert.ok(promptAndUpdate.notCalled); + }); + }); + + suite('Method promptAndUpdate()', () => { + const prompts = [Common.allow, Common.close]; + setup(() => { + workspaceService = TypeMoq.Mock.ofType(); + appShell = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + persistentStateFactory = mock(PersistentStateFactory); + notificationPromptEnabled = TypeMoq.Mock.ofType>(); + platformService = TypeMoq.Mock.ofType(); + applicationEnvironment = TypeMoq.Mock.ofType(); + applicationEnvironment.setup((a) => a.remoteName).returns(() => undefined); + when(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).thenReturn( + notificationPromptEnabled.object, + ); + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + appShell.object, + instance(persistentStateFactory), + + platformService.object, + applicationEnvironment.object, + ); + }); + + test('Does not display prompt if it is disabled', async () => { + notificationPromptEnabled + .setup((n) => n.value) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage, ...prompts)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + await condaInheritEnvPrompt.promptAndUpdate(); + verify(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).once(); + verifyAll(); + notificationPromptEnabled.verifyAll(); + }); + test('Do nothing if no option is selected', async () => { + const workspaceConfig = TypeMoq.Mock.ofType(); + notificationPromptEnabled + .setup((n) => n.value) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage, ...prompts)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal')) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.never()); + workspaceConfig + .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + notificationPromptEnabled + .setup((n) => n.updateValue(false)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + await condaInheritEnvPrompt.promptAndUpdate(); + verify(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).once(); + verifyAll(); + workspaceConfig.verifyAll(); + notificationPromptEnabled.verifyAll(); + }); + test('Update terminal settings if `Yes` is selected', async () => { + const workspaceConfig = TypeMoq.Mock.ofType(); + notificationPromptEnabled + .setup((n) => n.value) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage, ...prompts)) + .returns(() => Promise.resolve(Common.allow)) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal')) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.once()); + workspaceConfig + .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + notificationPromptEnabled + .setup((n) => n.updateValue(false)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + await condaInheritEnvPrompt.promptAndUpdate(); + verify(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).once(); + verifyAll(); + workspaceConfig.verifyAll(); + notificationPromptEnabled.verifyAll(); + }); + test('Disable notification prompt if `No` is selected', async () => { + const workspaceConfig = TypeMoq.Mock.ofType(); + notificationPromptEnabled + .setup((n) => n.value) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage, ...prompts)) + .returns(() => Promise.resolve(Common.close)) + .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()); + await condaInheritEnvPrompt.promptAndUpdate(); + verify(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).once(); + verifyAll(); + workspaceConfig.verifyAll(); + notificationPromptEnabled.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 3a7d8fde5cac..000000000000 --- a/src/test/interpreters/virtualEnvs/index.unit.test.ts +++ /dev/null @@ -1,273 +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 { InterpreterType, 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; - let processService: TypeMoq.IMock; - let pathUtils: TypeMoq.IMock; - let virtualEnvMgr: VirtualEnvironmentManager; - let fs: TypeMoq.IMock; - let workspace: TypeMoq.IMock; - let pipEnvService: TypeMoq.IMock; - let terminalActivation: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - - setup(() => { - const serviceContainer = TypeMoq.Mock.ofType(); - process = TypeMoq.Mock.ofType(); - processService = TypeMoq.Mock.ofType(); - const processFactory = TypeMoq.Mock.ofType(); - pathUtils = TypeMoq.Mock.ofType(); - fs = TypeMoq.Mock.ofType(); - workspace = TypeMoq.Mock.ofType(); - pipEnvService = TypeMoq.Mock.ofType(); - terminalActivation = TypeMoq.Mock.ofType(); - platformService = TypeMoq.Mock.ofType(); - - 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'); - terminalActivation - .setup(t => t.isShellSupported(TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - terminalActivation - .setup(t => t.getActivationCommandsForInterpreter!(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(['1'])) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const isRecognized = await virtualEnvMgr.isVirtualEnvironment(pythonPath); - - expect(isRecognized).to.be.equal(true, 'invalid value'); - terminalActivation.verifyAll(); - }); - - test(`Get Environment Type, does not detect virtualenv incorrectly ${testTitleSuffix}`, async () => { - const pythonPath = path.join('x', 'b', 'c', 'python'); - terminalActivation - .setup(t => t.isShellSupported(TypeMoq.It.isAny())) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - terminalActivation - .setup(t => t.getActivationCommandsForInterpreter!(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())) - .returns(() => Promise.resolve([])) - .verifiable(TypeMoq.Times.atLeastOnce()); - - let isRecognized = await virtualEnvMgr.isVirtualEnvironment(pythonPath); - - expect(isRecognized).to.be.equal(false, 'invalid value'); - terminalActivation.verifyAll(); - - terminalActivation.reset(); - terminalActivation - .setup(t => t.isShellSupported(TypeMoq.It.isAny())) - .returns(() => false) - .verifiable(TypeMoq.Times.atLeastOnce()); - terminalActivation - .setup(t => t.getActivationCommandsForInterpreter!(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny())) - .returns(() => Promise.resolve([])) - .verifiable(TypeMoq.Times.never()); - - isRecognized = await virtualEnvMgr.isVirtualEnvironment(pythonPath); - - expect(isRecognized).to.be.equal(false, 'invalid value'); - terminalActivation.verifyAll(); - }); - } - test('Get Environment Type, does not detect the type', async () => { - const pythonPath = path.join('x', 'b', 'c', 'python'); - virtualEnvMgr.isPipEnvironment = () => Promise.resolve(false); - virtualEnvMgr.isPyEnvEnvironment = () => Promise.resolve(false); - virtualEnvMgr.isVenvEnvironment = () => Promise.resolve(false); - virtualEnvMgr.isVirtualEnvironment = () => Promise.resolve(false); - - const envType = await virtualEnvMgr.getEnvironmentType(pythonPath); - - expect(envType).to.be.equal(InterpreterType.Unknown); - }); -}); diff --git a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts new file mode 100644 index 000000000000..2ad67831c455 --- /dev/null +++ b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +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'; +import { PersistentStateFactory } from '../../../client/common/persistentState'; +import { IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; +import { Common } from '../../../client/common/utils/localize'; +import { PythonPathUpdaterService } from '../../../client/interpreter/configuration/pythonPathUpdaterService'; +import { IPythonPathUpdaterServiceManager } from '../../../client/interpreter/configuration/types'; +import { IComponentAdapter, IInterpreterHelper, IInterpreterService } from '../../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../../client/interpreter/helpers'; +import { VirtualEnvironmentPrompt } from '../../../client/interpreter/virtualEnvs/virtualEnvPrompt'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as createEnvApi from '../../../client/pythonEnvironments/creation/createEnvApi'; + +suite('Virtual Environment Prompt', () => { + class VirtualEnvironmentPromptTest extends VirtualEnvironmentPrompt { + public async handleNewEnvironment(resource: Uri): Promise { + await super.handleNewEnvironment(resource); + } + + public async notifyUser(interpreter: PythonEnvironment, resource: Uri): Promise { + await super.notifyUser(interpreter, resource); + } + } + let persistentStateFactory: IPersistentStateFactory; + let helper: IInterpreterHelper; + let pythonPathUpdaterService: IPythonPathUpdaterServiceManager; + let disposable: Disposable; + let appShell: IApplicationShell; + let componentAdapter: IComponentAdapter; + let interpreterService: IInterpreterService; + let environmentPrompt: VirtualEnvironmentPromptTest; + let isCreatingEnvironmentStub: sinon.SinonStub; + setup(() => { + persistentStateFactory = mock(PersistentStateFactory); + helper = mock(InterpreterHelper); + pythonPathUpdaterService = mock(PythonPathUpdaterService); + componentAdapter = mock(); + interpreterService = mock(); + 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(persistentStateFactory), + instance(helper), + instance(pythonPathUpdaterService), + [instance(disposable)], + 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 notificationPromptEnabled = TypeMoq.Mock.ofType>(); + + 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(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>(); + + // 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>(); + + // 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(appShell.showInformationMessage(anything(), ...prompts)).once(); + }); + + test("If user selects 'Yes', python path is updated", async () => { + const resource = Uri.file('a'); + const interpreter1 = { path: 'path/to/interpreter1' }; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; + const notificationPromptEnabled = TypeMoq.Mock.ofType>(); + when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( + 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, + ), + ).thenResolve(); + + // 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)).once(); + verify( + pythonPathUpdaterService.updatePythonPath( + interpreter1.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + resource, + ), + ).once(); + }); + + test("If user selects 'No', no operation is performed", async () => { + const resource = Uri.file('a'); + const interpreter1 = { path: 'path/to/interpreter1' }; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; + const notificationPromptEnabled = TypeMoq.Mock.ofType>(); + when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( + 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, + ), + ).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(); + verify(appShell.showInformationMessage(anything(), ...prompts)).once(); + verify( + pythonPathUpdaterService.updatePythonPath( + interpreter1.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + resource, + ), + ).never(); + notificationPromptEnabled.verifyAll(); + }); + + 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 notificationPromptEnabled = TypeMoq.Mock.ofType>(); + when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( + 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(); + verify(appShell.showInformationMessage(anything(), ...prompts)).once(); + notificationPromptEnabled.verifyAll(); + }); + + test('If prompt is disabled, no notification is shown', async () => { + const resource = Uri.file('a'); + const interpreter1 = { path: 'path/to/interpreter1' }; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; + const notificationPromptEnabled = TypeMoq.Mock.ofType>(); + when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( + 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/interpreters/windowsRegistryService.unit.test.ts b/src/test/interpreters/windowsRegistryService.unit.test.ts deleted file mode 100644 index 82ae18b7f57e..000000000000 --- a/src/test/interpreters/windowsRegistryService.unit.test.ts +++ /dev/null @@ -1,315 +0,0 @@ -import * as assert from 'assert'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { 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 { WindowsRegistryService } from '../../client/interpreter/locators/services/windowsRegistryService'; -import { IServiceContainer } from '../../client/ioc/types'; -import { MockRegistry, MockState } from './mocks'; - -const environmentsPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'environments'); - -// tslint:disable:max-func-body-length no-octal-literal - -suite('Interpreters from Windows Registry (unit)', () => { - let serviceContainer: TypeMoq.IMock; - let interpreterHelper: TypeMoq.IMock; - let platformService: TypeMoq.IMock; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - const stateFactory = TypeMoq.Mock.ofType(); - interpreterHelper = TypeMoq.Mock.ofType(); - const pathUtils = TypeMoq.Mock.ofType(); - platformService = TypeMoq.Mock.ofType(); - 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); - pathUtils.setup(p => p.basename(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((p: string) => p.split(/[\\,\/]/).reverse()[0]); - const state = new MockState(undefined); - // tslint:disable-next-line:no-empty no-any - interpreterHelper.setup(h => h.getInterpreterInformation(TypeMoq.It.isAny())).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); - - 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); - - 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); - - 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, '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); - - 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, '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 () => { - 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); - - 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 () => { - 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); - 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'); - }); - test('Must return multiple entries', async () => { - 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); - 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 () => { - 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') }, - - // tslint:disable-next-line:no-any - { key: '\\Software\\Python\\Company Five\\8.0.0\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 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); - 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 () => { - 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') }, - - // tslint:disable-next-line:no-any - { key: '\\Software\\Python\\Company Five\\Five !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: 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); - 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/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(); + commandManager = mock(); + disposables = mock(); + + 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/characterStream.test.ts b/src/test/language/characterStream.test.ts deleted file mode 100644 index 63ea71f01746..000000000000 --- a/src/test/language/characterStream.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, TextRange } from '../../client/language/types'; - -// tslint:disable-next-line:max-func-body-length -suite('Language.CharacterStream', () => { - test('Iteration (string)', async () => { - const content = 'some text'; - const cs = new CharacterStream(content); - testIteration(cs, content); - }); - test('Iteration (iterator)', async () => { - const content = 'some text'; - const cs = new CharacterStream(new TextIterator(content)); - testIteration(cs, content); - }); - test('Positioning', async () => { - const content = 'some text'; - const cs = new CharacterStream(content); - assert.equal(cs.position, 0); - cs.advance(1); - assert.equal(cs.position, 1); - cs.advance(1); - assert.equal(cs.position, 2); - cs.advance(2); - assert.equal(cs.position, 4); - cs.advance(-3); - assert.equal(cs.position, 1); - cs.advance(-3); - assert.equal(cs.position, 0); - cs.advance(100); - assert.equal(cs.position, content.length); - }); - test('Characters', async () => { - const content = 'some \ttext "" \' \' \n text \r\n more text'; - const cs = new CharacterStream(content); - for (let i = 0; i < content.length; i += 1) { - assert.equal(cs.currentChar, content.charCodeAt(i)); - - assert.equal(cs.nextChar, i < content.length - 1 ? content.charCodeAt(i + 1) : 0); - assert.equal(cs.prevChar, i > 0 ? content.charCodeAt(i - 1) : 0); - - assert.equal(cs.lookAhead(2), i < content.length - 2 ? content.charCodeAt(i + 2) : 0); - assert.equal(cs.lookAhead(-2), i > 1 ? content.charCodeAt(i - 2) : 0); - - const ch = content.charCodeAt(i); - const isLineBreak = ch === Char.LineFeed || ch === Char.CarriageReturn; - assert.equal(cs.isAtWhiteSpace(), ch === Char.Tab || ch === Char.Space || isLineBreak); - assert.equal(cs.isAtLineBreak(), isLineBreak); - assert.equal(cs.isAtString(), ch === Char.SingleQuote || ch === Char.DoubleQuote); - - cs.moveNext(); - } - }); - test('Skip', async () => { - const content = 'some \ttext "" \' \' \n text \r\n more text'; - const cs = new CharacterStream(content); - - cs.skipWhitespace(); - assert.equal(cs.position, 0); - - cs.skipToWhitespace(); - assert.equal(cs.position, 4); - - cs.skipToWhitespace(); - assert.equal(cs.position, 4); - - cs.skipWhitespace(); - assert.equal(cs.position, 6); - - cs.skipLineBreak(); - assert.equal(cs.position, 6); - - cs.skipToEol(); - assert.equal(cs.position, 18); - - cs.skipLineBreak(); - assert.equal(cs.position, 19); - }); -}); - -function testIteration(cs: ICharacterStream, content: string) { - assert.equal(cs.position, 0); - assert.equal(cs.length, content.length); - assert.equal(cs.isEndOfStream(), false); - - for (let i = -2; i < content.length + 2; i += 1) { - const ch = cs.charCodeAt(i); - if (i < 0 || i >= content.length) { - assert.equal(ch, 0); - } else { - assert.equal(ch, content.charCodeAt(i)); - } - } - - for (let i = 0; i < content.length; i += 1) { - assert.equal(cs.isEndOfStream(), false); - assert.equal(cs.position, i); - assert.equal(cs.currentChar, content.charCodeAt(i)); - cs.moveNext(); - } - - assert.equal(cs.isEndOfStream(), true); - assert.equal(cs.position, content.length); -} diff --git a/src/test/language/languageConfiguration.unit.test.ts b/src/test/language/languageConfiguration.unit.test.ts new file mode 100644 index 000000000000..720d52a35476 --- /dev/null +++ b/src/test/language/languageConfiguration.unit.test.ts @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; + +import { getLanguageConfiguration } from '../../client/language/languageConfiguration'; + +const NEEDS_INDENT = [ + /^break$/, + /^continue$/, + // raise is indented unless it's the only thing on the line + /^raise\b/, + /^return\b/, +]; +const INDENT_ON_ENTER = [ + // block-beginning statements + /^async\s+def\b/, + /^async\s+for\b/, + /^async\s+with\b/, + /^class\b/, + /^def\b/, + /^with\b/, + /^try\b/, + /^except\b/, + /^finally\b/, + /^while\b/, + /^for\b/, + /^if\b/, + /^elif\b/, + /^else\b/, + /^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/, + /^break$/, + /^continue$/, + // 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 { + for (const regex of regexes) { + if (regex.test(line)) { + return true; + } + } + return false; +} + +function resolveExample( + base: string, + leading: string, + postKeyword: string, + preColon: string, + trailing: string, +): [string | undefined, string | undefined, boolean] { + let invalid: string | undefined; + if (base.trim() === '') { + invalid = 'blank line'; + } else if (leading === '' && isMember(base, NEEDS_INDENT)) { + invalid = 'expected indent'; + } else if (leading.trim() !== '') { + invalid = 'look-alike - pre-keyword'; + } else if (postKeyword.trim() !== '') { + invalid = 'look-alike - post-keyword'; + } + + let resolvedBase = base; + if (postKeyword !== '') { + if (resolvedBase.includes(' ')) { + const kw = resolvedBase.split(' ', 1)[0]; + const remainder = resolvedBase.substring(kw.length); + resolvedBase = `${kw}${postKeyword} ${remainder}`; + } else if (resolvedBase.endsWith(':')) { + resolvedBase = `${resolvedBase.substring(0, resolvedBase.length - 1)}${postKeyword}:`; + } else { + resolvedBase = `${resolvedBase}${postKeyword}`; + } + } + if (preColon !== '') { + if (resolvedBase.endsWith(':')) { + resolvedBase = `${resolvedBase.substring(0, resolvedBase.length - 1)}${preColon}:`; + } else { + return [undefined, undefined, true]; + } + } + const example = `${leading}${resolvedBase}${trailing}`; + return [example, invalid, false]; +} + +suite('Language Configuration', () => { + const cfg = getLanguageConfiguration(); + + suite('"brackets"', () => { + test('brackets is not defined', () => { + expect(cfg.brackets).to.be.equal(undefined, 'missing tests'); + }); + }); + + suite('"comments"', () => { + test('comments is not defined', () => { + expect(cfg.comments).to.be.equal(undefined, 'missing tests'); + }); + }); + + suite('"indentationRules"', () => { + test('indentationRules is not defined', () => { + expect(cfg.indentationRules).to.be.equal(undefined, 'missing tests'); + }); + }); + + suite('"onEnterRules"', () => { + const MULTILINE_SEPARATOR_INDENT_REGEX = cfg.onEnterRules![0].beforeText; + const INDENT_ONENTER_REGEX = cfg.onEnterRules![2].beforeText; + const OUTDENT_ONENTER_REGEX = cfg.onEnterRules![3].beforeText; + // To see the actual (non-verbose) regex patterns, un-comment + // the following lines: + // console.log(INDENT_ONENTER_REGEX.source); + // console.log(OUTDENT_ONENTER_REGEX.source); + + test('Multiline separator indent regex should not pick up strings with no multiline separator', async () => { + const result = MULTILINE_SEPARATOR_INDENT_REGEX.test('a = "test"'); + expect(result).to.be.equal( + false, + 'Multiline separator indent regex for regular strings should not have matches', + ); + }); + + test('Multiline separator indent regex should not pick up strings with escaped characters', async () => { + const result = MULTILINE_SEPARATOR_INDENT_REGEX.test("a = 'hello \\n'"); + expect(result).to.be.equal( + false, + 'Multiline separator indent regex for strings with escaped characters should not have matches', + ); + }); + + test('Multiline separator indent regex should pick up strings ending with a multiline separator', async () => { + const result = MULTILINE_SEPARATOR_INDENT_REGEX.test("a = 'multiline \\"); + expect(result).to.be.equal( + true, + 'Multiline separator indent regex for strings with newline separator should have matches', + ); + }); + + [ + // compound statements + 'async def test(self):', + 'async def :', + 'async :', + 'async for spam in bacon:', + 'async with context:', + 'async with context in manager:', + 'class Test:', + 'class Test(object):', + 'class :', + 'def spam():', + 'def spam(self, node, namespace=""):', + 'def :', + 'for item in items:', + 'for item in :', + 'for :', + 'if foo is None:', + 'if :', + 'try:', + "while '::' in macaddress:", + 'while :', + 'with self.test:', + 'with :', + 'elif x < 5:', + 'elif :', + 'else:', + 'except TestError:', + 'except :', + 'finally:', + '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)', + 'raise Exception', + 'raise', // re-raise + 'break', + 'continue', + 'return', + 'return True', + 'return (True, False, False)', + 'return [True, False, False]', + 'return {True, False, False}', + 'return (', + 'return [', + 'return {', + 'return', + // bogus + '', + ' ', + ' ', + ].forEach((base) => { + [ + ['', '', '', ''], + // leading + [' ', '', '', ''], + [' ', '', '', ''], // unusual indent + ['\t\t', '', '', ''], + // pre-keyword + ['x', '', '', ''], + // post-keyword + ['', 'x', '', ''], + // pre-colon + ['', '', ' ', ''], + // trailing + ['', '', '', ' '], + ['', '', '', '# a comment'], + ['', '', '', ' # ...'], + ].forEach((whitespace) => { + const [leading, postKeyword, preColon, trailing] = whitespace; + const [_example, invalid, ignored] = resolveExample(base, leading, postKeyword, preColon, trailing); + if (ignored) { + return; + } + const example = _example!; + + if (invalid) { + test(`Line "${example}" ignored (${invalid})`, () => { + let result: boolean; + + result = INDENT_ONENTER_REGEX.test(example); + expect(result).to.be.equal(false, 'unexpected match'); + + result = OUTDENT_ONENTER_REGEX.test(example); + expect(result).to.be.equal(false, 'unexpected match'); + }); + return; + } + + test(`Check indent-on-enter for line "${example}"`, () => { + let expected = false; + if (isMember(base, INDENT_ON_ENTER)) { + expected = true; + } + + const result = INDENT_ONENTER_REGEX.test(example); + + expect(result).to.be.equal(expected, 'unexpected result'); + }); + + test(`Check dedent-on-enter for line "${example}"`, () => { + let expected = false; + if (isMember(base, DEDENT_ON_ENTER)) { + expected = true; + } + + const result = OUTDENT_ONENTER_REGEX.test(example); + + expect(result).to.be.equal(expected, 'unexpected result'); + }); + }); + }); + }); + + suite('"wordPattern"', () => { + test('wordPattern is not defined', () => { + expect(cfg.wordPattern).to.be.equal(undefined, 'missing tests'); + }); + }); +}); diff --git a/src/test/language/textIterator.test.ts b/src/test/language/textIterator.test.ts deleted file mode 100644 index 34daa81534cd..000000000000 --- a/src/test/language/textIterator.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.test.ts b/src/test/language/textRange.test.ts deleted file mode 100644 index 02cad753c16f..000000000000 --- a/src/test/language/textRange.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( - () => { const e = new TextRange(0, -1); }, - Error - ); - assert.throws( - () => { const e = TextRange.fromBounds(3, 1); }, - Error - ); - }); -}); diff --git a/src/test/language/textRangeCollection.test.ts b/src/test/language/textRangeCollection.test.ts deleted file mode 100644 index 53e5ff4dc650..000000000000 --- a/src/test/language/textRangeCollection.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.test.ts b/src/test/language/tokenizer.test.ts deleted file mode 100644 index f90da3eeffd0..000000000000 --- a/src/test/language/tokenizer.test.ts +++ /dev/null @@ -1,383 +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 { 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]}`); - } - }); -}); 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 = () => Promise.resolve(); + + const workspaceService = ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: (listener: (event: ConfigurationChangeEvent) => Promise) => { + 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 = () => Promise.resolve(); + + const workspaceService = ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: (listener: (event: ConfigurationChangeEvent) => Promise) => { + 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 = () => + Promise.resolve(); + + const workspaceService = ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: (listener: (event: WorkspaceFoldersChangeEvent) => Promise) => { + 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 = () => Promise.resolve(); + + const interpreterService = ({ + onDidChangeInterpreterInformation: ( + listener: (event: PythonEnvironment) => Promise, + 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 = () => Promise.resolve(); + + const interpreterService = ({ + onDidChangeInterpreterInformation: ( + listener: (event: PythonEnvironment) => Promise, + 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 = () => Promise.resolve(); + + const interpreterService = ({ + onDidChangeInterpreterInformation: ( + listener: (event: PythonEnvironment) => Promise, + 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 = () => Promise.resolve(); + + const interpreterService = ({ + onDidChangeInterpreterInformation: ( + listener: (event: PythonEnvironment) => Promise, + 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 a92c853e1dec..000000000000 --- a/src/test/languageServers/jedi/autocomplete/base.test.ts +++ /dev/null @@ -1,253 +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 { isPythonVersion } from '../../../common'; -import { closeActiveWindows, initialize, initializeTest } from '../../../initialize'; -import { UnitTestIocContainer } from '../../../unittests/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; - - suiteSetup(async function () { - // Attempt to fix #1301 - // tslint:disable-next-line:no-invalid-this - this.timeout(60000); - 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('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.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.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); - const list = await vscode.commands.executeCommand('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.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.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.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.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.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.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.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', done => { - 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.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.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 a6e89f0e37c1..000000000000 --- a/src/test/languageServers/jedi/autocomplete/pep484.test.ts +++ /dev/null @@ -1,63 +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 '../../../unittests/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.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.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 3c7d5bb98e57..000000000000 --- a/src/test/languageServers/jedi/autocomplete/pep526.test.ts +++ /dev/null @@ -1,99 +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 '../../../unittests/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.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.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.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.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.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.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 096bbd5c3423..000000000000 --- a/src/test/languageServers/jedi/completionSource.unit.test.ts +++ /dev/null @@ -1,89 +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>; - let autoCompleteSettings: TypeMoq.IMock; - let itemInfoSource: TypeMoq.IMock; - setup(() => { - const jediFactory = TypeMoq.Mock.ofType(JediFactory); - jediHandler = TypeMoq.Mock.ofType>(); - const serviceContainer = TypeMoq.Mock.ofType(); - const configService = TypeMoq.Mock.ofType(); - const pythonSettings = TypeMoq.Mock.ofType(); - autoCompleteSettings = TypeMoq.Mock.ofType(); - autoCompleteSettings = TypeMoq.Mock.ofType(); - - 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(); - completionSource = new CompletionSource(jediFactory.object, serviceContainer.object, itemInfoSource.object); - }); - - async function testDocumentation(source: string, addBrackets: boolean) { - const doc = TypeMoq.Mock.ofType(); - const position = new Position(1, 1); - const token = new CancellationTokenSource().token; - const lineText = TypeMoq.Mock.ofType(); - const completionResult = TypeMoq.Mock.ofType(); - - 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 38ade0be494c..000000000000 --- a/src/test/languageServers/jedi/definitions/hover.jedi.test.ts +++ /dev/null @@ -1,282 +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 { 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)', () => { - suiteSetup(initialize); - 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.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', done => { - 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.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'); - // tslint:disable-next-line:prefer-template - assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + 'def fun()' + EOL + '```' + EOL + 'This is fun', 'Invalid conents'); - }).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.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'); - // tslint:disable-next-line:prefer-template - assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + 'def bar()' + EOL + '```' + EOL + - '说明 - keep this line, it works' + EOL + 'delete following line, it works' + - EOL + '如果存在需要等待审批或正在执行的任务,将不刷新页面', 'Invalid conents'); - }).then(done, done); - }); - - test('Across files with Unicode Characters', done => { - 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.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'); - // tslint:disable-next-line:prefer-template - assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + - 'def showMessage()' + EOL + - '```' + EOL + - 'Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи. ' + EOL + - 'Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку.', 'Invalid conents'); - }).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.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.executeHoverProvider', textDocument.uri, position); - }).then(def => { - assert.equal(def!.length, 0, 'Definition length is incorrect'); - }).then(done, done); - }); - - test('Highlighting Class', 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(11, 15); - return vscode.commands.executeCommand('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'); - // tslint:disable-next-line:prefer-template - const documentation = '```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 conents'); - }).then(done, done); - }); - - test('Highlight Method', 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(12, 10); - return vscode.commands.executeCommand('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'); - // tslint:disable-next-line:prefer-template - assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + - 'def randint(a, b)' + EOL + - '```' + EOL + - 'Return random integer in range [a, b], including both end points.', 'Invalid conents'); - }).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.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'); - // tslint:disable-next-line:prefer-template - assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + - 'def acos(x)' + EOL + - '```' + EOL + - 'Return the arc cosine (measured in radians) of x.', 'Invalid conents'); - }).then(done, done); - }); - - test('Highlight Multiline Method Signature', 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(14, 14); - return vscode.commands.executeCommand('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'); - // tslint:disable-next-line:prefer-template - assert.equal(normalizeMarkedString(def[0].contents[0]), '```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.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.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 a403ba7668b8..000000000000 --- a/src/test/languageServers/jedi/definitions/navigation.test.ts +++ /dev/null @@ -1,130 +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 { closeActiveWindows, initialize, initializeTest } from '../../../initialize'; - -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', () => { - suiteSetup(initialize); - setup(initializeTest); - suiteTeardown(closeActiveWindows); - teardown(closeActiveWindows); - - 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.executeDefinitionProvider', textDocument.uri, startPosition); - assert.equal(expectedFiles.length, locations!.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', buildTest( - fileUsages, - new vscode.Position(7, 1), - [fileDefinitions], - [new vscode.Range(2, 0, 11, 17)] - )); - - test('Specifically imported function decorated by stdlib', buildTest( - fileUsages, - new vscode.Position(14, 6), - [fileDefinitions], - [new vscode.Range(21, 0, 27, 17)] - )); - - test('Specifically imported function decorated by local decorator', buildTest( - fileUsages, - new vscode.Position(15, 6), - [fileDefinitions], - [new vscode.Range(14, 0, 18, 7)] - )); -}); 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 bc3563e6fc8a..000000000000 --- a/src/test/languageServers/jedi/definitions/parallel.jedi.test.ts +++ /dev/null @@ -1,49 +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.executeHoverProvider', textDocument.uri, position); - const codeDef = await vscode.commands.executeCommand('vscode.executeDefinitionProvider', textDocument.uri, position); - position = new vscode.Position(3, 10); - const list = await vscode.commands.executeCommand('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 d33a69db8d65..000000000000 --- a/src/test/languageServers/jedi/pythonSignatureProvider.unit.test.ts +++ /dev/null @@ -1,250 +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>; - let argResultItems: IArgumentsResult; - setup(() => { - const jediFactory = TypeMoq.Mock.ofType(JediFactory); - jediHandler = TypeMoq.Mock.ofType>(); - 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 { - const doc = TypeMoq.Mock.ofType(); - const position = new Position(0, pos); - const lineText = TypeMoq.Mock.ofType(); - const argsResult = TypeMoq.Mock.ofType(); - const cancelToken = TypeMoq.Mock.ofType(); - 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(); - 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); - argsResult.setup(c => c.definitions).returns(() => argResultItems[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 = TypeMoq.Mock.ofType(); - textLine.setup(t => t.text).returns(() => sourceLine); - const doc: TypeMoq.IMock = TypeMoq.Mock.ofType(); - 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 6ca6f3f2b344..000000000000 --- a/src/test/languageServers/jedi/signature/signature.jedi.test.ts +++ /dev/null @@ -1,145 +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 '../../../unittests/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 () => { - const 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, 'stop'), - new SignatureHelpResult(0, 7, 1, 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 - this.skip(); - return; - } - const expected = [ - new SignatureHelpResult(0, 5, 0, 0, null), - new SignatureHelpResult(0, 6, 1, 0, 'value'), - new SignatureHelpResult(0, 7, 1, 0, 'value'), - new SignatureHelpResult(0, 8, 1, 1, '...'), - new SignatureHelpResult(0, 9, 1, 1, '...'), - new SignatureHelpResult(0, 10, 1, 1, '...'), - new SignatureHelpResult(0, 11, 1, 2, 'sep'), - new SignatureHelpResult(0, 12, 1, 2, 'sep') - ]; - - 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, 1, 0, 'x'); - } else { - expected = new SignatureHelpResult(0, 4, 1, 0, null); - } - - const document = await openDocument(path.join(autoCompPath, 'noSigPy3.py')); - await checkSignature(expected, document!.uri, 0); - }); -}); - -async function openDocument(documentPath: string): Promise { - 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.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 768b30959028..000000000000 --- a/src/test/languageServers/jedi/symbolProvider.unit.test.ts +++ /dev/null @@ -1,464 +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'; -import { IFileSystem } from '../../../client/common/platform/types'; -import { splitParent } from '../../../client/common/utils/string'; -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; - let jediHandler: TypeMoq.IMock>; - let jediFactory: TypeMoq.IMock; - let fileSystem: TypeMoq.IMock; - let provider: DocumentSymbolProvider; - let uri: Uri; - let doc: TypeMoq.IMock; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - jediFactory = TypeMoq.Mock.ofType(JediFactory); - jediHandler = TypeMoq.Mock.ofType>(); - - fileSystem = TypeMoq.Mock.ofType(); - doc = TypeMoq.Mock.ofType(); - 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(); - - 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(); - symbols.setup((s: any) => s.then).returns(() => undefined); - const definitions: IDefinition[] = []; - for (let counter = 0; counter < 3; counter += 1) { - const def = TypeMoq.Mock.ofType(); - 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 { - const langClient = TypeMoq.Mock.ofType(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":"","scheme":"file"},"range":[{"line":0,"character":7},{"line":0,"character":15}]},"containerName":""},{"name":"SpamTests","kind":4,"location":{"uri":{"$mid":1,"path":"","scheme":"file"},"range":[{"line":2,"character":0},{"line":4,"character":29}]},"containerName":""},{"name":"test_all","kind":11,"location":{"uri":{"$mid":1,"path":"","scheme":"file"},"range":[{"line":3,"character":4},{"line":4,"character":29}]},"containerName":"SpamTests"},{"name":"self","kind":12,"location":{"uri":{"$mid":1,"path":"","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 { - const doc = TypeMoq.Mock.ofType(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[SymbolKind[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; -} 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/lint.args.test.ts b/src/test/linters/lint.args.test.ts deleted file mode 100644 index d9826c6a056f..000000000000 --- a/src/test/linters/lint.args.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 - -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, ILogger, 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 { Pep8 } from '../../client/linters/pep8'; -import { Prospector } from '../../client/linters/prospector'; -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; - let engine: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let docManager: TypeMoq.IMock; - let settings: TypeMoq.IMock; - let lm: ILinterManager; - let serviceContainer: ServiceContainer; - let document: TypeMoq.IMock; - let outputChannel: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - 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(); - - const fs = TypeMoq.Mock.ofType(); - fs.setup(x => x.fileExists(TypeMoq.It.isAny())).returns(() => new Promise((resolve, reject) => resolve(true))); - fs.setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => true); - serviceManager.addSingletonInstance(IFileSystem, fs.object); - - serviceManager.addSingletonInstance(IOutputChannel, outputChannel.object); - - interpreterService = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); - serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); - engine = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(ILintingEngine, engine.object); - - docManager = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IDocumentManager, docManager.object); - - const lintSettings = TypeMoq.Mock.ofType(); - lintSettings.setup(x => x.enabled).returns(() => true); - lintSettings.setup(x => x.lintOnSave).returns(() => true); - - settings = TypeMoq.Mock.ofType(); - settings.setup(x => x.linting).returns(() => lintSettings.object); - - configService = TypeMoq.Mock.ofType(); - configService.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceManager.addSingletonInstance(IConfigurationService, configService.object); - - const workspaceFolder: WorkspaceFolder | undefined = workspaceUri ? { uri: Uri.file(workspaceUri), index: 0, name: '' } : undefined; - workspaceService = TypeMoq.Mock.ofType(); - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); - serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); - - const logger = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(ILogger, logger.object); - - const installer = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IInstaller, installer.object); - - const platformService = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IPlatformService, platformService.object); - - lm = new LinterManager(serviceContainer, workspaceService.object); - serviceManager.addSingletonInstance(ILinterManager, lm); - document = TypeMoq.Mock.ofType(); - }); - - async function testLinter(linter: BaseLinter, expectedArgs: string[]) { - document.setup(d => d.uri).returns(() => fileUri); - - let invoked = false; - (linter as any).run = (args, doc, token) => { - 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('Pep8', async () => { - const linter = new Pep8(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, doc, token) => { - 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.manager.unit.test.ts b/src/test/linters/lint.manager.unit.test.ts deleted file mode 100644 index 9b7778b7d7cc..000000000000 --- a/src/test/linters/lint.manager.unit.test.ts +++ /dev/null @@ -1,101 +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'; - -// setup class instance -class TestLinterManager extends LinterManager { - public enableUnconfiguredLintersCallCount: number = 0; - - protected async enableUnconfiguredLinters(resource?: Uri): Promise { - this.enableUnconfiguredLintersCallCount += 1; - } -} - -function getServiceContainerMockForLinterManagerTests(): TypeMoq.IMock { - // setup test mocks - const serviceContainerMock = TypeMoq.Mock.ofType(); - const configMock = TypeMoq.Mock.ofType(); - const pythonSettingsMock = TypeMoq.Mock.ofType(); - configMock.setup(cm => cm.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettingsMock.object); - serviceContainerMock.setup(c => c.get(IConfigurationService)).returns(() => configMock.object); - - return serviceContainerMock; -} - -// tslint:disable-next-line:max-func-body-length -suite('Lint Manager Unit Tests', () => { - const workspaceService = TypeMoq.Mock.ofType(); - 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 936799bd3e9f..000000000000 --- a/src/test/linters/lint.multilinter.test.ts +++ /dev/null @@ -1,106 +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 { 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), :1\n1,7,E,E999:SyntaxError: invalid syntax\n'; - public pylintMsg: string = '************* Module print\ns:142:13), :1\n1,0,error,syntax-error:Missing parentheses in call to \'print\'. Did you mean print(x)? (, line 1)\n'; - - // Depending on moduleName being exec'd, return the appropriate sample. - public async exec(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise> { - 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); - linterManager = api.serviceContainer.get(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, 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, 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('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); - - 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 36a4b7c2a0c6..000000000000 --- a/src/test/linters/lint.multiroot.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import * as assert from 'assert'; -import * as path from 'path'; -import { CancellationTokenSource, ConfigurationTarget, OutputChannel, Uri, workspace } from 'vscode'; -import { PythonSettings } from '../../client/common/configSettings'; -import { CTagsProductPathService, 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/unittests/common/constants'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { UnitTestIocContainer } from '../unittests/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.registerPlatformTypes(); - ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton(IProductPathService, CTagsProductPathService, ProductType.WorkspaceSymbols); - ioc.serviceManager.addSingleton(IProductPathService, FormatterProductPathService, ProductType.Formatter); - ioc.serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); - ioc.serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); - ioc.serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); - - } - - async function createLinter(product: Product, resource?: Uri): Promise { - const mockOutputChannel = ioc.serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); - const lm = ioc.serviceContainer.get(ILinterManager); - await lm.setActiveLintersAsync([product], resource); - return lm.createLinter(product, mockOutputChannel, ioc.serviceContainer); - } - async function testLinterInWorkspaceFolder(product: Product, workspaceFolderRelativePath: string, mustHaveErrors: boolean): Promise { - 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); - } - async function enableDisableSetting(workspaceFolder, configTarget: ConfigurationTarget, setting: string, value: boolean): Promise { - const config = ioc.serviceContainer.get(IConfigurationService); - await config.updateSetting(setting, value, Uri.file(workspaceFolder), configTarget); - } - - test('Enabling Pylint in root and also in Workspace, should return errors', async () => { - await runTest(Product.pylint, true, true, pylintSetting); - }); - test('Enabling Pylint in root and disabling in Workspace, should not return errors', async () => { - await runTest(Product.pylint, true, false, pylintSetting); - }); - test('Disabling Pylint in root and enabling in Workspace, should return errors', async () => { - await runTest(Product.pylint, false, true, pylintSetting); - }); - - test('Enabling Flake8 in root and also in Workspace, should return errors', async () => { - await runTest(Product.flake8, true, true, flake8Setting); - }); - test('Enabling Flake8 in root and disabling in Workspace, should not return errors', async () => { - await runTest(Product.flake8, true, false, flake8Setting); - }); - test('Disabling Flake8 in root and enabling in Workspace, should return errors', async () => { - await runTest(Product.flake8, false, true, flake8Setting); - }); - - async function runTest(product: Product, global: boolean, wks: boolean, setting: string): Promise { - const expected = wks ? wks : global; - await enableDisableSetting(multirootPath, ConfigurationTarget.Global, setting, global); - await enableDisableSetting(multirootPath, ConfigurationTarget.Workspace, setting, wks); - await testLinterInWorkspaceFolder(product, 'workspace1', expected); - } -}); diff --git a/src/test/linters/lint.provider.test.ts b/src/test/linters/lint.provider.test.ts deleted file mode 100644 index 458f469b9a3b..000000000000 --- a/src/test/linters/lint.provider.test.ts +++ /dev/null @@ -1,182 +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 { - IApplicationShell, IDocumentManager, IWorkspaceService -} from '../../client/common/application/types'; -import { IFileSystem } from '../../client/common/platform/types'; -import { - IConfigurationService, IInstaller, ILintingSettings, - IPythonSettings, Product -} 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'; - -// tslint:disable-next-line:max-func-body-length -suite('Linting - Provider', () => { - let context: TypeMoq.IMock; - let interpreterService: TypeMoq.IMock; - let engine: TypeMoq.IMock; - let configService: TypeMoq.IMock; - let docManager: TypeMoq.IMock; - let settings: TypeMoq.IMock; - let lm: ILinterManager; - let serviceContainer: ServiceContainer; - let emitter: vscode.EventEmitter; - let document: TypeMoq.IMock; - let fs: TypeMoq.IMock; - let appShell: TypeMoq.IMock; - let linterInstaller: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - - suiteSetup(initialize); - setup(async () => { - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - - serviceContainer = new ServiceContainer(cont); - context = TypeMoq.Mock.ofType(); - - fs = TypeMoq.Mock.ofType(); - fs.setup(x => x.fileExists(TypeMoq.It.isAny())).returns(() => new Promise((resolve, reject) => resolve(true))); - fs.setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => true); - serviceManager.addSingletonInstance(IFileSystem, fs.object); - - interpreterService = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IInterpreterService, interpreterService.object); - - engine = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(ILintingEngine, engine.object); - - docManager = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IDocumentManager, docManager.object); - - const lintSettings = TypeMoq.Mock.ofType(); - lintSettings.setup(x => x.enabled).returns(() => true); - lintSettings.setup(x => x.lintOnSave).returns(() => true); - - settings = TypeMoq.Mock.ofType(); - settings.setup(x => x.linting).returns(() => lintSettings.object); - - configService = TypeMoq.Mock.ofType(); - configService.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceManager.addSingletonInstance(IConfigurationService, configService.object); - - appShell = TypeMoq.Mock.ofType(); - linterInstaller = TypeMoq.Mock.ofType(); - workspaceService = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IApplicationShell, appShell.object); - serviceManager.addSingletonInstance(IInstaller, linterInstaller.object); - serviceManager.addSingletonInstance(IWorkspaceService, workspaceService.object); - serviceManager.add(IAvailableLinterActivator, AvailableLinterActivator); - serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); - lm = new LinterManager(serviceContainer, workspaceService.object); - serviceManager.addSingletonInstance(ILinterManager, lm); - emitter = new vscode.EventEmitter(); - document = TypeMoq.Mock.ofType(); - }); - - test('Lint on open file', () => { - 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'); - - // tslint:disable-next-line:no-unused-variable - const provider = new LinterProvider(context.object, serviceContainer); - 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'); - - // tslint:disable-next-line:no-unused-variable - const provider = new LinterProvider(context.object, serviceContainer); - emitter.fire(document.object); - engine.verify(x => x.lintDocument(document.object, 'save'), TypeMoq.Times.once()); - }); - - test('No lint on open other files', () => { - 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'); - - // tslint:disable-next-line:no-unused-variable - const provider = new LinterProvider(context.object, serviceContainer); - emitter.fire(document.object); - engine.verify(x => x.lintDocument(document.object, 'save'), TypeMoq.Times.never()); - }); - - test('No lint on save other files', () => { - 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'); - - // tslint:disable-next-line:no-unused-variable - const provider = new LinterProvider(context.object, serviceContainer); - emitter.fire(document.object); - engine.verify(x => x.lintDocument(document.object, 'save'), TypeMoq.Times.never()); - }); - - test('Lint on change interpreters', () => { - const e = new vscode.EventEmitter(); - interpreterService.setup(x => x.onDidChangeInterpreter).returns(() => e.event); - - // tslint:disable-next-line:no-unused-variable - const provider = new LinterProvider(context.object, serviceContainer); - 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]); - // tslint:disable-next-line:no-unused-variable - const provider = new LinterProvider(context.object, serviceContainer); - emitter.fire(document.object); - - const deferred = createDeferred(); - setTimeout(() => deferred.resolve(), 2000); - await deferred.promise; - engine.verify(x => x.lintOpenPythonFiles(), TypeMoq.Times.once()); - }); - - test('Diagnostic cleared on file close', () => testClearDiagnosticsOnClose(true)); - test('Diagnostic not cleared on file opened in another tab', () => testClearDiagnosticsOnClose(false)); - - 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]); - // tslint:disable-next-line:prefer-const no-unused-variable - const provider = new LinterProvider(context.object, serviceContainer); - - 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 db4a3fdcbfa3..000000000000 --- a/src/test/linters/lint.test.ts +++ /dev/null @@ -1,300 +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 * as path from 'path'; -import { CancellationTokenSource, ConfigurationTarget, Uri, workspace } from 'vscode'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; -import { Product } from '../../client/common/installer/productInstaller'; -import { - CTagsProductPathService, 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, ProductType } from '../../client/common/types'; -import { LinterManager } from '../../client/linters/linterManager'; -import { ILinterManager, ILintMessage, LintMessageSeverity } from '../../client/linters/types'; -import { deleteFile, PythonSettingKeys, rootWorkspaceUri } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { MockOutputChannel } from '../mockClasses'; -import { UnitTestIocContainer } from '../unittests/serviceRegistry'; - -const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); -const pythoFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'linting'); -const flake8ConfigPath = path.join(pythoFilesPath, 'flake8config'); -const pep8ConfigPath = path.join(pythoFilesPath, 'pep8config'); -const pydocstyleConfigPath27 = path.join(pythoFilesPath, 'pydocstyleconfig27'); -const pylintConfigPath = path.join(pythoFilesPath, 'pylintconfig'); -const fileToLint = path.join(pythoFilesPath, 'file.py'); -const threeLineLintsPath = path.join(pythoFilesPath, 'threeLineLints.py'); - -const pylintMessagesToBeReturned: ILintMessage[] = [ - { line: 24, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: '' }, - { line: 30, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: '' }, - { line: 34, column: 0, severity: LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', provider: '', type: '' }, - { line: 40, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: '' }, - { line: 44, column: 0, severity: LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', provider: '', type: '' }, - { line: 55, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: '' }, - { line: 59, column: 0, severity: LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', provider: '', type: '' }, - { line: 62, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling undefined-variable (E0602)', provider: '', type: '' }, - { line: 70, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: '' }, - { line: 84, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: '' }, - { line: 87, column: 0, severity: LintMessageSeverity.Hint, code: 'C0304', message: 'Final newline missing', provider: '', type: '' }, - { line: 11, column: 20, severity: LintMessageSeverity.Warning, code: 'W0613', message: 'Unused argument \'arg\'', provider: '', type: '' }, - { line: 26, column: 14, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blop\' member', provider: '', type: '' }, - { line: 36, column: 14, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, - { line: 46, column: 18, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, - { line: 61, column: 18, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, - { line: 72, column: 18, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, - { line: 75, column: 18, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, - { line: 77, column: 14, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' }, - { line: 83, column: 14, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: '' } -]; -const flake8MessagesToBeReturned: ILintMessage[] = [ - { line: 5, column: 1, severity: LintMessageSeverity.Error, code: 'E302', message: 'expected 2 blank lines, found 1', provider: '', type: '' }, - { line: 19, column: 15, severity: LintMessageSeverity.Error, code: 'E127', message: 'continuation line over-indented for visual indent', provider: '', type: '' }, - { line: 24, column: 23, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: '' }, - { line: 62, column: 30, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: '' }, - { line: 70, column: 22, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: '' }, - { line: 80, column: 5, severity: LintMessageSeverity.Error, code: 'E303', message: 'too many blank lines (2)', provider: '', type: '' }, - { line: 87, column: 24, severity: LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: '' } -]; -const pep8MessagesToBeReturned: ILintMessage[] = [ - { line: 5, column: 1, severity: LintMessageSeverity.Error, code: 'E302', message: 'expected 2 blank lines, found 1', provider: '', type: '' }, - { line: 19, column: 15, severity: LintMessageSeverity.Error, code: 'E127', message: 'continuation line over-indented for visual indent', provider: '', type: '' }, - { line: 24, column: 23, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: '' }, - { line: 62, column: 30, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: '' }, - { line: 70, column: 22, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: '' }, - { line: 80, column: 5, severity: LintMessageSeverity.Error, code: 'E303', message: 'too many blank lines (2)', provider: '', type: '' }, - { line: 87, column: 24, severity: LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: '' } -]; -const pydocstyleMessagseToBeReturned: 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 filteredPep88MessagesToBeReturned: ILintMessage[] = [ - { line: 87, column: 24, severity: LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: '' } -]; - -// tslint:disable-next-line:max-func-body-length -suite('Linting - General Tests', () => { - let ioc: UnitTestIocContainer; - let linterManager: ILinterManager; - let configService: IConfigurationService; - - suiteSetup(async function () { - // these tests are currently flakey, and need some refactoring to become reliable - // See #3914 - // tslint:disable-next-line:no-invalid-this - return this.skip(); - - await initialize(); - }); - setup(async () => { - initializeDI(); - await initializeTest(); - await resetSettings(); - }); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await ioc.dispose(); - await closeActiveWindows(); - await resetSettings(); - await deleteFile(path.join(workspaceUri.fsPath, '.pylintrc')); - await deleteFile(path.join(workspaceUri.fsPath, '.pydocstyle')); - }); - - 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); - ioc.serviceManager.addSingletonInstance(IProductService, new ProductService()); - ioc.serviceManager.addSingleton(IProductPathService, CTagsProductPathService, ProductType.WorkspaceSymbols); - ioc.serviceManager.addSingleton(IProductPathService, FormatterProductPathService, ProductType.Formatter); - ioc.serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); - ioc.serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); - ioc.serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); - } - - 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; - } - - async function testEnablingDisablingOfLinter(product: Product, enabled: boolean, file?: string) { - // Reenable this after fixing #3922 - // const setting = makeSettingKey(product); - // const output = ioc.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - - // await configService.updateSetting(setting, enabled, rootWorkspaceUri, - // IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace); - - // file = file ? file : fileToLint; - // const document = await workspace.openTextDocument(file); - // const cancelToken = new CancellationTokenSource(); - - // await linterManager.setActiveLintersAsync([product]); - // await linterManager.enableLintingAsync(enabled); - // const linter = await linterManager.createLinter(product, output, ioc.serviceContainer); - - // const messages = await linter.lint(document, cancelToken.token); - // if (enabled) { - // assert.notEqual(messages.length, 0, `No linter errors when linter is enabled, Output - ${output.output}`); - // } else { - // assert.equal(messages.length, 0, `Errors returned when linter is disabled, Output - ${output.output}`); - // } - } - - test('Disable Pylint and test linter', async () => { - await testEnablingDisablingOfLinter(Product.pylint, false); - }); - test('Enable Pylint and test linter', async () => { - await testEnablingDisablingOfLinter(Product.pylint, true); - }); - test('Disable Pep8 and test linter', async () => { - await testEnablingDisablingOfLinter(Product.pep8, false); - }); - test('Enable Pep8 and test linter', async () => { - await testEnablingDisablingOfLinter(Product.pep8, true); - }); - test('Disable Flake8 and test linter', async () => { - await testEnablingDisablingOfLinter(Product.flake8, false); - }); - test('Enable Flake8 and test linter', async () => { - await testEnablingDisablingOfLinter(Product.flake8, true); - }); - test('Disable Prospector and test linter', async function () { - // Skipping to solve #3464, tracked by issue #3466. - // tslint:disable-next-line:no-invalid-this - return this.skip(); - await testEnablingDisablingOfLinter(Product.prospector, false); - }); - test('Enable Prospector and test linter', async function () { - // Skipping to solve #3464, tracked by issue #3466. - // tslint:disable-next-line:no-invalid-this - return this.skip(); - await testEnablingDisablingOfLinter(Product.prospector, true); - }); - test('Disable Pydocstyle and test linter', async () => { - await testEnablingDisablingOfLinter(Product.pydocstyle, false); - }); - test('Enable Pydocstyle and test linter', async () => { - await testEnablingDisablingOfLinter(Product.pydocstyle, true); - }); - - // tslint:disable-next-line:no-any - async function testLinterMessages(product: Product, pythonFile: string, messagesToBeReceived: ILintMessage[]): Promise { - const outputChannel = ioc.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - const cancelToken = new CancellationTokenSource(); - const document = await workspace.openTextDocument(pythonFile); - - await linterManager.setActiveLintersAsync([product], document.uri); - const linter = await linterManager.createLinter(product, outputChannel, ioc.serviceContainer); - - const messages = await linter.lint(document, cancelToken.token); - if (messagesToBeReceived.length === 0) { - assert.equal(messages.length, 0, `No errors in linter, Output - ${outputChannel.output}`); - } else { - if (outputChannel.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 - ${outputChannel.output}`); - } - } - } - test('PyLint', async () => { - await testLinterMessages(Product.pylint, fileToLint, pylintMessagesToBeReturned); - }); - test('Flake8', async () => { - await testLinterMessages(Product.flake8, fileToLint, flake8MessagesToBeReturned); - }); - test('Pep8', async () => { - await testLinterMessages(Product.pep8, fileToLint, pep8MessagesToBeReturned); - }); - test('Pydocstyle', async () => { - await testLinterMessages(Product.pydocstyle, fileToLint, pydocstyleMessagseToBeReturned); - }); - test('PyLint with config in root', async () => { - await fs.copy(path.join(pylintConfigPath, '.pylintrc'), path.join(workspaceUri.fsPath, '.pylintrc')); - await testLinterMessages(Product.pylint, path.join(pylintConfigPath, 'file2.py'), []); - }); - test('Flake8 with config in root', async () => { - await testLinterMessages(Product.flake8, path.join(flake8ConfigPath, 'file.py'), filteredFlake8MessagesToBeReturned); - }); - test('Pep8 with config in root', async () => { - await testLinterMessages(Product.pep8, path.join(pep8ConfigPath, 'file.py'), filteredPep88MessagesToBeReturned); - }); - test('Pydocstyle with config in root', async () => { - await configService.updateSetting('linting.pylintUseMinimalCheckers', false, workspaceUri); - await fs.copy(path.join(pydocstyleConfigPath27, '.pydocstyle'), path.join(workspaceUri.fsPath, '.pydocstyle')); - await testLinterMessages(Product.pydocstyle, path.join(pydocstyleConfigPath27, 'file.py'), []); - }); - test('PyLint minimal checkers', async () => { - const file = path.join(pythoFilesPath, 'minCheck.py'); - await configService.updateSetting('linting.pylintUseMinimalCheckers', true, workspaceUri); - await testEnablingDisablingOfLinter(Product.pylint, false, file); - await configService.updateSetting('linting.pylintUseMinimalCheckers', false, workspaceUri); - await testEnablingDisablingOfLinter(Product.pylint, true, file); - }); - // tslint:disable-next-line:no-any - async function testLinterMessageCount(product: Product, pythonFile: string, messageCountToBeReceived: number): Promise { - const outputChannel = ioc.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - const cancelToken = new CancellationTokenSource(); - const document = await workspace.openTextDocument(pythonFile); - - await linterManager.setActiveLintersAsync([product], document.uri); - const linter = await linterManager.createLinter(product, outputChannel, ioc.serviceContainer); - - const messages = await linter.lint(document, cancelToken.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 target = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - await configService.updateSetting('linting.maxNumberOfProblems', maxErrors, rootWorkspaceUri, target); - await testLinterMessageCount(Product.pylint, threeLineLintsPath, maxErrors); - }); -}); diff --git a/src/test/linters/lintengine.test.ts b/src/test/linters/lintengine.test.ts deleted file mode 100644 index f34bce78b720..000000000000 --- a/src/test/linters/lintengine.test.ts +++ /dev/null @@ -1,125 +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; - let lintManager: TypeMoq.IMock; - let settings: TypeMoq.IMock; - let lintSettings: TypeMoq.IMock; - let fileSystem: TypeMoq.IMock; - let lintingEngine: ILintingEngine; - - suiteSetup(initialize); - setup(async () => { - serviceContainer = TypeMoq.Mock.ofType(); - - const docManager = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager), TypeMoq.It.isAny())).returns(() => docManager.object); - - const workspaceService = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())).returns(() => workspaceService.object); - - fileSystem = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())).returns(() => fileSystem.object); - - lintSettings = TypeMoq.Mock.ofType(); - settings = TypeMoq.Mock.ofType(); - - const configService = TypeMoq.Mock.ofType(); - 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(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(STANDARD_OUTPUT_CHANNEL))).returns(() => outputChannel.object); - - lintManager = TypeMoq.Mock.ofType(); - 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(); - 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 bed92c6e5d74..000000000000 --- a/src/test/linters/linter.availability.unit.test.ts +++ /dev/null @@ -1,447 +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, WorkspaceConfiguration } from 'vscode'; -import { - IApplicationShell, IWorkspaceService -} from '../../client/common/application/types'; -import { - IConfigurationService, IInstaller, IPythonSettings, Product -} from '../../client/common/types'; -import { AvailableLinterActivator } from '../../client/linters/linterAvailability'; -import { LinterInfo } from '../../client/linters/linterInfo'; -import { IAvailableLinterActivator } from '../../client/linters/types'; - -// tslint:disable-next-line:max-func-body-length -suite('Linter Availability Provider tests', () => { - - test('Availability feature is disabled when global default for jediEnabled=true.', async () => { - // set expectations - const jediEnabledValue = true; - const expectedResult = false; - - // arrange - const [appShellMock, installerMock, workspaceServiceMock, configServiceMock] = getDependenciesForAvailabilityTests(); - setupConfigurationServiceForJediSettingsTest(jediEnabledValue, configServiceMock); - - // call - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, installerMock.object, workspaceServiceMock.object, configServiceMock.object); - - // check expectaions - expect(availabilityProvider.isFeatureEnabled).is.equal(expectedResult, 'Avaialability feature should be disabled when python.jediEnabled is true'); - workspaceServiceMock.verifyAll(); - }); - - test('Availability feature is enabled when global default for jediEnabled=false.', async () => { - // set expectations - const jediEnabledValue = false; - const expectedResult = true; - - // arrange - const [appShellMock, installerMock, workspaceServiceMock, configServiceMock] = getDependenciesForAvailabilityTests(); - setupConfigurationServiceForJediSettingsTest(jediEnabledValue, configServiceMock); - - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, installerMock.object, workspaceServiceMock.object, configServiceMock.object); - - expect(availabilityProvider.isFeatureEnabled).is.equal(expectedResult, 'Avaialability feature should be enabled when python.jediEnabled defaults to false'); - 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, installerMock, workspaceServiceMock, configServiceMock, linterInfo] = getDependenciesForAvailabilityTests(); - setupWorkspaceMockForLinterConfiguredTests(pylintUserValue, pylintWorkspaceValue, pylintWorkspaceFolderValue, workspaceServiceMock); - - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, installerMock.object, workspaceServiceMock.object, configServiceMock.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, installerMock, workspaceServiceMock, configServiceMock, linterInfo] = getDependenciesForAvailabilityTests(); - setupWorkspaceMockForLinterConfiguredTests(pylintUserValue, pylintWorkspaceValue, pylintWorkspaceFolderValue, workspaceServiceMock); - - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, installerMock.object, workspaceServiceMock.object, configServiceMock.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, installerMock, workspaceServiceMock, configServiceMock, linterInfo] = getDependenciesForAvailabilityTests(); - setupWorkspaceMockForLinterConfiguredTests(pylintUserValue, pylintWorkspaceValue, pylintWorkspaceFolderValue, workspaceServiceMock); - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, installerMock.object, workspaceServiceMock.object, configServiceMock.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, installerMock, workspaceServiceMock, configServiceMock, linterInfo] = getDependenciesForAvailabilityTests(); - setupWorkspaceMockForLinterConfiguredTests(pylintUserValue, pylintWorkspaceValue, pylintWorkspaceFolderValue, workspaceServiceMock); - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, installerMock.object, workspaceServiceMock.object, configServiceMock.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(promptReply: { title: string; enabled: boolean } | undefined): Promise { - // arrange - const [appShellMock, installerMock, workspaceServiceMock] = getDependenciesForAvailabilityTests(); - const configServiceMock = TypeMoq.Mock.ofType(); - - const linterInfo = new class extends LinterInfo { - public testIsEnabled: boolean = promptReply ? promptReply.enabled : false; - - public async enableAsync(enabled: boolean, resource?: Uri): Promise { - this.testIsEnabled = enabled; - return Promise.resolve(); - } - - }(Product.pylint, 'pylint', configServiceMock.object, ['.pylintrc', 'pylintrc']); - - appShellMock.setup(ap => ap.showInformationMessage( - TypeMoq.It.isValue(`Linter ${linterInfo.id} is installed but not enabled.`), - TypeMoq.It.isAny(), - TypeMoq.It.isAny()) - ) - .returns(() => { - // tslint:disable-next-line:no-any - return promptReply as any; - }) - .verifiable(TypeMoq.Times.once()); - - // perform test - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, installerMock.object, workspaceServiceMock.object, configServiceMock.object); - const result = await availabilityProvider.promptToConfigureAvailableLinter(linterInfo); - if (promptReply) { - expect(linterInfo.testIsEnabled).to.equal(promptReply.enabled, 'LinterInfo test class was not updated as a result of the test.'); - } - - return result; - } - - test('Linter is enabled after being prompted and "Enable " is selected', async () => { - // set expectations - const expectedResult = true; - const promptReply = { title: 'Enable pylint', enabled: true }; - - // run scenario - const result = await testForLinterPromptResponse(promptReply); - - // test results - expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return true because the configuration was updated.'); - }); - - test('Linter is disabled after being prompted and "Disable " is selected', async () => { - // set expectation - const promptReply = { title: 'Disable pylint', enabled: false }; - const expectedResult = true; - - // run scenario - const result = await testForLinterPromptResponse(promptReply); - - // test results - expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return true because the configuration was updated.'); - }); - - test('Linter is left unconfigured after being prompted and the prompt is disabled without any selection made', async () => { - // set expectation - const promptReply = undefined; - const expectedResult = false; - - // run scenario - const result = await testForLinterPromptResponse(promptReply); - - // test results - expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return true because the configuration was updated.'); - }); - - // Options to test the implementation of the IAvailableLinterActivator. - // All options default to values that would otherwise allow the prompt to appear. - class AvailablityTestOverallOptions { - public jediEnabledValue: boolean = false; - public pylintUserEnabled?: boolean; - public pylintWorkspaceEnabled?: boolean; - public pylintWorkspaceFolderEnabled?: boolean; - public linterIsInstalled: boolean = true; - public promptReply?: { title: string; enabled: boolean }; - } - - async function performTestOfOverallImplementation(options: AvailablityTestOverallOptions): Promise { - // arrange - const [appShellMock, installerMock, workspaceServiceMock, configServiceMock, linterInfo] = getDependenciesForAvailabilityTests(); - appShellMock.setup(ap => ap.showInformationMessage( - TypeMoq.It.isValue(`Linter ${linterInfo.id} is installed but not enabled.`), - TypeMoq.It.isAny(), - TypeMoq.It.isAny()) - ) - // tslint:disable-next-line:no-any - .returns(() => options.promptReply as any) - .verifiable(TypeMoq.Times.once()); - - installerMock.setup(im => im.isInstalled(linterInfo.product, TypeMoq.It.isAny())) - .returns(async () => options.linterIsInstalled) - .verifiable(TypeMoq.Times.once()); - - setupConfigurationServiceForJediSettingsTest(options.jediEnabledValue, configServiceMock); - setupWorkspaceMockForLinterConfiguredTests( - options.pylintUserEnabled, - options.pylintWorkspaceEnabled, - options.pylintWorkspaceFolderEnabled, - workspaceServiceMock - ); - - // perform test - const availabilityProvider: IAvailableLinterActivator = new AvailableLinterActivator(appShellMock.object, installerMock.object, workspaceServiceMock.object, configServiceMock.object); - return availabilityProvider.promptIfLinterAvailable(linterInfo); - } - - test('Overall implementation does not change configuration when feature disabled', async () => { - // set expectations - const testOpts = new AvailablityTestOverallOptions(); - testOpts.jediEnabledValue = true; - const expectedResult = false; - - // arrange - const result = await performTestOfOverallImplementation(testOpts); - - // perform test - expect(expectedResult).to.equal(result, 'promptIfLinterAvailable should not change any configuration when python.jediEnabled is true.'); - }); - - 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.promptReply = 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 changes configuration when user is prompted and "Disable " is selected', async () => { - // set expectations - const testOpts = new AvailablityTestOverallOptions(); - testOpts.promptReply = { title: 'Disable pylint', enabled: false }; - 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('Overall implementation changes configuration when user is prompted and "Enable " is selected', async () => { - // set expectations - const testOpts = new AvailablityTestOverallOptions(); - testOpts.promptReply = { title: 'Enable pylint', enabled: true }; - 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, installerMock, workspaceServiceMock, configServiceMock, linterInfo] = getDependenciesForAvailabilityTests(); - setupInstallerForAvailabilityTest(linterInfo, linterIsInstalled, installerMock); - - // perform test - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, installerMock.object, workspaceServiceMock.object, configServiceMock.object); - const result = await availabilityProvider.isLinterAvailable(linterInfo.product); - - expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return true because the configuration was updated.'); - installerMock.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, installerMock, workspaceServiceMock, configServiceMock, linterInfo] = getDependenciesForAvailabilityTests(); - setupInstallerForAvailabilityTest(linterInfo, linterIsInstalled, installerMock); - - // perform test - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, installerMock.object, workspaceServiceMock.object, configServiceMock.object); - const result = await availabilityProvider.isLinterAvailable(linterInfo.product); - - expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return true because the configuration was updated.'); - installerMock.verifyAll(); - }); - - test('Discovery of linter is available in the environment returns false when it fails', async () => { - // set expectations - const expectedResult = false; - - // arrange - const [appShellMock, installerMock, workspaceServiceMock, configServiceMock, linterInfo] = getDependenciesForAvailabilityTests(); - installerMock.setup(im => im.isInstalled(linterInfo.product, TypeMoq.It.isAny())) - .returns(() => Promise.reject('error testfail')) - .verifiable(TypeMoq.Times.once()); - - // perform test - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, installerMock.object, workspaceServiceMock.object, configServiceMock.object); - const result = await availabilityProvider.isLinterAvailable(linterInfo.product); - - expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return true because the configuration was updated.'); - installerMock.verifyAll(); - }); -}); - -function setupWorkspaceMockForLinterConfiguredTests( - enabledForUser: boolean | undefined, - enabeldForWorkspace: boolean | undefined, - enabledForWorkspaceFolder: boolean | undefined, - workspaceServiceMock?: TypeMoq.IMock): TypeMoq.IMock { - - if (!workspaceServiceMock) { - workspaceServiceMock = TypeMoq.Mock.ofType(); - } - const workspaceConfiguration = TypeMoq.Mock.ofType(); - 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( - jediEnabledValue: boolean, - configServiceMock: TypeMoq.IMock -): [ - TypeMoq.IMock, - TypeMoq.IMock - ] { - - if (!configServiceMock) { - configServiceMock = TypeMoq.Mock.ofType(); - } - const pythonSettings = TypeMoq.Mock.ofType(); - pythonSettings.setup(ps => ps.jediEnabled).returns(() => jediEnabledValue); - - configServiceMock.setup(cs => cs.getSettings()).returns(() => pythonSettings.object); - return [configServiceMock, pythonSettings]; -} - -function setupInstallerForAvailabilityTest(linterInfo: LinterInfo, linterIsInstalled: boolean, installerMock): TypeMoq.IMock { - if (!installerMock) { - installerMock = TypeMoq.Mock.ofType(); - } - installerMock.setup(im => im.isInstalled(linterInfo.product, TypeMoq.It.isAny())) - .returns(async () => linterIsInstalled) - .verifiable(TypeMoq.Times.once()); - - return installerMock; -} - -function getDependenciesForAvailabilityTests(): [ - TypeMoq.IMock, - TypeMoq.IMock, - TypeMoq.IMock, - TypeMoq.IMock, - LinterInfo -] { - const configServiceMock = TypeMoq.Mock.ofType(); - return [ - TypeMoq.Mock.ofType(), - TypeMoq.Mock.ofType(), - TypeMoq.Mock.ofType(), - TypeMoq.Mock.ofType(), - new LinterInfo(Product.pylint, '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 ff244b893d1e..000000000000 --- a/src/test/linters/linterCommands.unit.test.ts +++ /dev/null @@ -1,168 +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 { 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)).thenReturn(instance(manager)); - when(svcContainer.get(IApplicationShell)).thenReturn(instance(shell)); - when(svcContainer.get(IDocumentManager)).thenReturn(instance(docManager)); - when(svcContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - when(svcContainer.get(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']), 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 f6b253b58724..000000000000 --- a/src/test/linters/linterManager.unit.test.ts +++ /dev/null @@ -1,181 +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)).thenReturn(instance(shell)); - when(svcContainer.get(IDocumentManager)).thenReturn(instance(docManager)); - when(svcContainer.get(ICommandManager)).thenReturn(instance(cmdManager)); - when(svcContainer.get(ILintingEngine)).thenReturn(instance(lintingEngine)); - when(svcContainer.get(IConfigurationService)).thenReturn(instance(configService)); - when(svcContainer.get(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) - .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).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(); - const linterInstances = new Map(); - 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 1784a7b61842..000000000000 --- a/src/test/linters/linterinfo.unit.test.ts +++ /dev/null @@ -1,74 +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 { anything, instance, mock, when } from 'ts-mockito'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { PylintLinterInfo } from '../../client/linters/linterInfo'; - -suite('Linter Info - Pylint', () => { - test('Test disabled when Pylint is explicitly disabled', async () => { - const config = mock(ConfigurationService); - const workspaceService = mock(WorkspaceService); - const linterInfo = new PylintLinterInfo(instance(config), instance(workspaceService), []); - - when(config.getSettings(anything())).thenReturn({ linting: { pylintEnabled: false } } as any); - - expect(linterInfo.isEnabled()).to.be.false; - }); - test('Test disabled when Jedi is enabled and Pylint is explicitly disabled', async () => { - const config = mock(ConfigurationService); - const workspaceService = mock(WorkspaceService); - const linterInfo = new PylintLinterInfo(instance(config), instance(workspaceService), []); - - when(config.getSettings(anything())).thenReturn({ linting: { pylintEnabled: false }, jediEnabled: true } as any); - - expect(linterInfo.isEnabled()).to.be.false; - }); - test('Test enabled when Jedi is enabled and Pylint is explicitly enabled', async () => { - const config = mock(ConfigurationService); - const workspaceService = mock(WorkspaceService); - const linterInfo = new PylintLinterInfo(instance(config), instance(workspaceService), []); - - when(config.getSettings(anything())).thenReturn({ linting: { pylintEnabled: true }, jediEnabled: true } as any); - - expect(linterInfo.isEnabled()).to.be.true; - }); - test('Test disabled when using Language Server and Pylint is not configured', async () => { - const config = mock(ConfigurationService); - const workspaceService = mock(WorkspaceService); - const linterInfo = new PylintLinterInfo(instance(config), instance(workspaceService), []); - - const inspection = {}; - const pythonConfig = { - inspect: () => inspection - }; - when(config.getSettings(anything())).thenReturn({ linting: { pylintEnabled: true }, jediEnabled: false } as any); - when(workspaceService.getConfiguration('python', anything())).thenReturn(pythonConfig as any); - - expect(linterInfo.isEnabled()).to.be.false; - }); - test('Test enabled when using Language Server and Pylint is configured', async () => { - const config = mock(ConfigurationService); - const workspaceService = mock(WorkspaceService); - const linterInfo = new PylintLinterInfo(instance(config), instance(workspaceService), []); - - const inspection = { - globalValue: 'something', - workspaceFolderValue: 'something', - workspaceValue: 'something' - }; - const pythonConfig = { - inspect: () => inspection - }; - when(config.getSettings(anything())).thenReturn({ linting: { pylintEnabled: true }, jediEnabled: false } 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 1f6e36146c1b..000000000000 --- a/src/test/linters/mypy.unit.test.ts +++ /dev/null @@ -1,56 +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 } 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, '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 459f233ffc89..000000000000 --- a/src/test/linters/pylint.test.ts +++ /dev/null @@ -1,248 +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, WorkspaceFolder } from 'vscode'; -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, ILogger, 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; - let platformService: TypeMoq.IMock; - let workspace: TypeMoq.IMock; - let execService: TypeMoq.IMock; - let config: TypeMoq.IMock; - let serviceContainer: ServiceContainer; - - setup(() => { - fileSystem = TypeMoq.Mock.ofType(); - fileSystem - .setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) - .returns((a, b) => a === b); - - platformService = TypeMoq.Mock.ofType(); - platformService.setup(x => x.isWindows).returns(() => false); - - workspace = TypeMoq.Mock.ofType(); - execService = TypeMoq.Mock.ofType(); - - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - serviceContainer = new ServiceContainer(cont); - - serviceManager.addSingletonInstance(IFileSystem, fileSystem.object); - serviceManager.addSingletonInstance(IWorkspaceService, workspace.object); - serviceManager.addSingletonInstance(IPythonToolExecutionService, execService.object); - serviceManager.addSingletonInstance(IPlatformService, platformService.object); - serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); - config = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(IConfigurationService, config.object); - const linterManager = new LinterManager(serviceContainer, workspace.object); - serviceManager.addSingletonInstance(ILinterManager, linterManager); - const logger = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(ILogger, logger.object); - const installer = TypeMoq.Mock.ofType(); - serviceManager.addSingletonInstance(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.hasConfigrationFileInWorkspace(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 { - const outputChannel = TypeMoq.Mock.ofType(); - const pylinter = new Pylint(outputChannel.object, serviceContainer); - - const document = TypeMoq.Mock.ofType(); - document.setup(x => x.uri).returns(() => Uri.file(path.join(fileFolder, 'test.py'))); - - const wsf = TypeMoq.Mock.ofType(); - 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(); - settings.setup(x => x.linting).returns(() => lintSettings); - 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(); - const pylinter = new Pylint(outputChannel.object, serviceContainer); - - const document = TypeMoq.Mock.ofType(); - document.setup(x => x.uri).returns(() => Uri.file(path.join(fileFolder, 'test.py'))); - - const wsf = TypeMoq.Mock.ofType(); - 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(); - settings.setup(x => x.linting).returns(() => lintSettings); - 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/markdown/restTextConverter.test.ts b/src/test/markdown/restTextConverter.test.ts deleted file mode 100644 index ee08a8a9f2d6..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 { - 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 () => await testConversion('scipy')); - test('scipy.spatial', async () => await testConversion('scipy.spatial')); - test('scipy.spatial.distance', async () => await testConversion('scipy.spatial.distance')); - test('anydbm', async () => await testConversion('anydbm')); - test('aifc', async () => await testConversion('aifc')); - test('astroid', async () => await testConversion('astroid')); -}); diff --git a/src/test/mockClasses.ts b/src/test/mockClasses.ts index a0f665d50f71..e2de7e649b87 100644 --- a/src/test/mockClasses.ts +++ b/src/test/mockClasses.ts @@ -1,86 +1,74 @@ import * as vscode from 'vscode'; -import { - Flake8CategorySeverity, ILintingSettings, IMypyCategorySeverity, - IPep8CategorySeverity, 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; + public isShown!: boolean; + private _eventEmitter = new vscode.EventEmitter(); + public onDidChangeLogLevel: vscode.Event = 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; } - public appendLine(value: string) { this.append(value); this.append('\n'); } - // tslint:disable-next-line:no-empty - public clear() { } + public appendLine(value: string) { + this.append(value); + this.append('\n'); + } + + 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 { + + public show(_x?: any, _y?: any): void { this.isShown = true; } public hide() { this.isShown = false; } - // tslint:disable-next-line:no-empty - public dispose() { } + + public dispose() {} } export class MockStatusBarItem implements vscode.StatusBarItem { - public alignment: vscode.StatusBarAlignment; - public priority: number; - public text: string; - public tooltip: string; - public color: string; - public command: string; - // tslint:disable-next-line:no-empty - public show(): void { - } - // tslint:disable-next-line:no-empty - public hide(): void { - } - // tslint:disable-next-line:no-empty - public dispose(): void { - } -} + 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; + public id: string = ''; + public name: string = ''; + + public show(): void {} + + public hide(): 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 pep8Enabled: boolean; - public pep8Args: 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 pep8CategorySeverity: IPep8CategorySeverity; - public flake8CategorySeverity: Flake8CategorySeverity; - public mypyCategorySeverity: IMypyCategorySeverity; - public prospectorPath: string; - public pylintPath: string; - public pep8Path: 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 43e4262d285b..cc4ab4ddb8e5 100644 --- a/src/test/mocks/autoSelector.ts +++ b/src/test/mocks/autoSelector.ts @@ -6,27 +6,38 @@ import { injectable } from 'inversify'; import { Event, EventEmitter } from 'vscode'; import { Resource } from '../../client/common/types'; -import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/types'; -import { PythonInterpreter } from '../../client/interpreter/contracts'; +import { + IInterpreterAutoSelectionService, + IInterpreterAutoSelectionProxyService, +} from '../../client/interpreter/autoSelection/types'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; @injectable() -export class MockAutoSelectionService implements IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService { - public async setWorkspaceInterpreter(_resource: Resource, _interpreter: PythonInterpreter): Promise { +export class MockAutoSelectionService + implements IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService { + // eslint-disable-next-line class-methods-use-this + public async setWorkspaceInterpreter(_resource: Resource, _interpreter: PythonEnvironment): Promise { return Promise.resolve(); } - public async setGlobalInterpreter(_interpreter: PythonInterpreter): Promise { - return; - } + + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + public async setGlobalInterpreter(_interpreter: PythonEnvironment): Promise {} + + // eslint-disable-next-line class-methods-use-this get onDidChangeAutoSelectedInterpreter(): Event { return new EventEmitter().event; } + + // eslint-disable-next-line class-methods-use-this public autoSelectInterpreter(_resource: Resource): Promise { 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 implements Extension { + id!: string; + extensionUri!: Uri; + extensionPath!: string; + isActive!: boolean; + packageJSON: any; + extensionKind!: ExtensionKind; + exports!: T; + activate(): Thenable { + 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[] = []; + onDidChange: Event = () => { + throw new Error('Method not implemented'); + }; + getExtension(extensionId: string): Extension | undefined; + getExtension(extensionId: string): Extension | undefined; + getExtension(extensionId: unknown): import('vscode').Extension | 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( + targetCtor?: common.CtorWithArgs, + behavior?: TypeMoq.MockBehavior, + shouldOverrideTarget?: boolean, + ...targetCtorArgs: any[] +): TypeMoq.IMock { + // 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(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 f2dbaefc1d70..1ffa09884262 100644 --- a/src/test/mocks/mementos.ts +++ b/src/test/mocks/mementos.ts @@ -3,17 +3,34 @@ import { Memento } from 'vscode'; @injectable() export class MockMemento implements Memento { - private map: Map = new Map(); - // tslint:disable-next-line:no-any + // 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 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _value: Record = {}; + + 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(key: string, defaultValue?: T): T { - const exists = this.map.has(key); - // tslint:disable-next-line:no-any - return exists ? this.map.get(key) : defaultValue! as any; + const exists = this._value.hasOwnProperty(key); + + // 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 { - this.map.set(key, value); + this._value[key] = value; return Promise.resolve(); } + + 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; + + 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 { + 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/mocks/mockDocument.ts b/src/test/mocks/mockDocument.ts new file mode 100644 index 000000000000..a9cd39985311 --- /dev/null +++ b/src/test/mocks/mockDocument.ts @@ -0,0 +1,233 @@ +/* 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'; + +class MockLine implements TextLine { + private _range: Range; + + private _rangeWithLineBreak: Range; + + private _firstNonWhitespaceIndex: number | undefined; + + private _isEmpty: boolean | undefined; + + constructor(private _contents: string, private _line: number, private _offset: number) { + this._range = new Range(new Position(_line, 0), new Position(_line, _contents.length)); + this._rangeWithLineBreak = new Range(this.range.start, new Position(_line, _contents.length + 1)); + } + + public get offset(): number { + return this._offset; + } + + public get lineNumber(): number { + return this._line; + } + + public get text(): string { + return this._contents; + } + + public get range(): Range { + return this._range; + } + + public get rangeIncludingLineBreak(): Range { + return this._rangeWithLineBreak; + } + + public get firstNonWhitespaceCharacterIndex(): number { + if (this._firstNonWhitespaceIndex === undefined) { + this._firstNonWhitespaceIndex = this._contents.trimLeft().length - this._contents.length; + } + return this._firstNonWhitespaceIndex; + } + + public get isEmptyOrWhitespace(): boolean { + if (this._isEmpty === undefined) { + this._isEmpty = this._contents.length === 0 || this._contents.trim().length === 0; + } + return this._isEmpty; + } +} + +export class MockDocument implements TextDocument { + private _uri: Uri; + + private _version = 0; + + private _lines: MockLine[] = []; + + private _contents = ''; + + private _isUntitled = false; + + private _isDirty = false; + + private _language = 'python'; + + private _onSave: (doc: TextDocument) => Promise; + + constructor( + contents: string, + fileName: string, + onSave: (doc: TextDocument) => Promise, + 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): void { + this._contents = contents; + this._lines = this.createLines(); + } + + public addContent(contents: string): void { + this.setContent(`${this._contents}\n${contents}`); + } + + public forceUntitled(): void { + this._isUntitled = true; + this._isDirty = true; + } + + public get uri(): Uri { + return this._uri; + } + + public get fileName(): string { + return this._uri.fsPath; + } + + public get isUntitled(): boolean { + return this._isUntitled; + } + + public get languageId(): string { + return 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 { + 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]; + } + 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; + } + 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 && position.line > 0) { + // use default when custom-regexp isn't provided + regexp = /a/; + } + + 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; + } + + public edit(c: TextDocumentContentChangeEvent): void { + this._version += 1; + const before = this._contents.substr(0, c.rangeOffset); + const after = this._contents.substr(c.rangeOffset + c.rangeLength); + this._contents = `${before}${c.text}${after}`; + this._lines = this.createLines(); + } + + private createLines(): MockLine[] { + const split = this._contents.split('\n'); + let prevLine: MockLine | undefined; + return split.map((s, i) => { + const nextLine = this.createTextLine(s, i, prevLine); + prevLine = nextLine; + return nextLine; + }); + } + + // 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, + ); + } + + private convertToOffset(pos: Position): number { + if (pos.line < this._lines.length) { + return ( + this._lines[pos.line].offset + + Math.min(this._lines[pos.line].rangeIncludingLineBreak.end.character, pos.character) + ); + } + return this._contents.length; + } +} diff --git a/src/test/mocks/mockDocumentManager.ts b/src/test/mocks/mockDocumentManager.ts new file mode 100644 index 000000000000..43134fb5fc02 --- /dev/null +++ b/src/test/mocks/mockDocumentManager.ts @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { + DecorationRenderOptions, + Event, + EventEmitter, + Range, + TextDocument, + TextDocumentChangeEvent, + TextDocumentShowOptions, + TextEditor, + TextEditorDecorationType, + TextEditorOptionsChangeEvent, + TextEditorSelectionChangeEvent, + TextEditorViewColumnChangeEvent, + Uri, + ViewColumn, + WorkspaceEdit, +} from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../../client/constants'; +import { MockDocument } from './mockDocument'; +import { MockEditor } from './mockTextEditor'; +import { IMockDocumentManager } from './mockTypes'; + +export class MockDocumentManager implements IMockDocumentManager { + public textDocuments: TextDocument[] = []; + + public activeTextEditor: TextEditor | undefined; + + public visibleTextEditors: TextEditor[] = []; + + public didChangeActiveTextEditorEmitter = new EventEmitter(); + + private didOpenEmitter = new EventEmitter(); + + private didChangeVisibleEmitter = new EventEmitter(); + + private didChangeTextEditorSelectionEmitter = new EventEmitter(); + + private didChangeTextEditorOptionsEmitter = new EventEmitter(); + + private didChangeTextEditorViewColumnEmitter = new EventEmitter(); + + private didCloseEmitter = new EventEmitter(); + + private didSaveEmitter = new EventEmitter(); + + private didChangeTextDocumentEmitter = new EventEmitter(); + + public get onDidChangeActiveTextEditor(): Event { + return this.didChangeActiveTextEditorEmitter.event; + } + + public get onDidChangeTextDocument(): Event { + return this.didChangeTextDocumentEmitter.event; + } + + public get onDidOpenTextDocument(): Event { + return this.didOpenEmitter.event; + } + + public get onDidChangeVisibleTextEditors(): Event { + return this.didChangeVisibleEmitter.event; + } + + public get onDidChangeTextEditorSelection(): Event { + return this.didChangeTextEditorSelectionEmitter.event; + } + + public get onDidChangeTextEditorOptions(): Event { + return this.didChangeTextEditorOptionsEmitter.event; + } + + public get onDidChangeTextEditorViewColumn(): Event { + return this.didChangeTextEditorViewColumnEmitter.event; + } + + public get onDidCloseTextDocument(): Event { + return this.didCloseEmitter.event; + } + + public get onDidSaveTextDocument(): Event { + return this.didSaveEmitter.event; + } + + public showTextDocument( + _document: TextDocument, + _column?: ViewColumn, + _preserveFocus?: boolean, + ): Thenable; + + public showTextDocument(_document: TextDocument | Uri, _options?: TextDocumentShowOptions): Thenable; + + public showTextDocument(document: unknown, _column?: unknown, _preserveFocus?: unknown): Thenable { + 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; + + public openTextDocument(_options?: { language?: string; content?: string }): Thenable; + + public openTextDocument(_options?: unknown): Thenable { + 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 { + throw new Error('Method not implemented.'); + } + + 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, language); + this.textDocuments.push(existing); + } + return existing; + } + + 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) => { + const startOffset = doc.offsetAt(c.range.start); + const endOffset = doc.offsetAt(c.range.end); + return { + range: c.range, + rangeOffset: startOffset, + rangeLength: endOffset - startOffset, + text: c.newText, + }; + }); + const ev: TextDocumentChangeEvent = { + document: doc, + contentChanges, + reason: undefined, + }; + // Changes are applied to the doc before it's sent. + ev.contentChanges.forEach(doc.edit.bind(doc)); + this.didChangeTextDocumentEmitter.fire(ev); + } + } + + // eslint-disable-next-line class-methods-use-this + public createTextEditorDecorationType(_options: DecorationRenderOptions): TextEditorDecorationType { + throw new Error('Method not implemented'); + } + + private get lastDocument(): TextDocument { + if (this.textDocuments.length > 0) { + return this.textDocuments[this.textDocuments.length - 1]; + } + throw new Error('No documents in MockDocumentManager'); + } + + private saveDocument = (doc: TextDocument): Promise => { + // Create a new document with the contents of the doc passed in + this.addDocument(doc.getText(), path.join(EXTENSION_ROOT_DIR, 'baz.py')); + return Promise.resolve(true); + }; +} diff --git a/src/test/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 { + 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 { + 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 = { + 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(); + + constructor(defaultSettings?: { [key: string]: unknown }) { + if (defaultSettings) { + const keys = [...Object.keys(defaultSettings)]; + keys.forEach((k) => this.values.set(k, defaultSettings[k])); + } + } + + public get(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(section: string): SectionType | undefined { + return this.values.get(section) as SectionType; + } + + public update( + section: string, + value: unknown, + _configurationTarget?: boolean | ConfigurationTarget | undefined, + ): Promise { + this.values.set(section, value); + return Promise.resolve(); + } +} diff --git a/src/test/mocks/moduleInstaller.ts b/src/test/mocks/moduleInstaller.ts index 565c1c63c662..fb183e6ebd99 100644 --- a/src/test/mocks/moduleInstaller.ts +++ b/src/test/mocks/moduleInstaller.ts @@ -1,18 +1,34 @@ 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 { + + public async installModule(name: Product | string, _resource?: Uri): Promise { this.emit('installModule', name); } - public async isSupported(resource?: Uri): Promise { + + public async isSupported(_resource?: Uri): Promise { return this.supported; } } diff --git a/src/test/mocks/proc.ts b/src/test/mocks/proc.ts index 857a9d6c369c..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,42 +23,56 @@ export class MockProcessService extends EventEmitter implements IProcessService constructor(private procService: IProcessService) { super(); } - public onExecObservable(handler: (file: string, args: string[], options: SpawnOptions, callback: ExecObservableCallback) => void) { + + public onExecObservable( + handler: (file: string, args: string[], options: SpawnOptions, callback: ExecObservableCallback) => void, + ): void { this.on('execObservable', handler); } + public execObservable(file: string, args: string[], options: SpawnOptions = {}): ObservableExecutionResult { let value: Observable> | Output | undefined; let valueReturned = false; - this.emit('execObservable', file, args, options, (result: Observable> | Output) => { value = result; valueReturned = true; }); + this.emit('execObservable', file, args, options, (result: Observable> | Output) => { + value = result; + valueReturned = true; + }); if (valueReturned) { const output = value as Output; - if (['stderr', 'stdout'].some(source => source === output.source)) { + 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>, - dispose: () => { noop(); } + dispose: () => { + noop(); + }, }; } - } else { - return this.procService.execObservable(file, args, options); + return { + proc: {} as ChildProcess, + out: value as Observable>, + 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> { let value: ExecutionResult | undefined; let valueReturned = false; - this.emit('exec', file, args, options, (result: ExecutionResult) => { value = result; valueReturned = true; }); + this.emit('exec', file, args, options, (result: ExecutionResult) => { + value = result; + valueReturned = true; + }); return valueReturned ? value! : this.procService.exec(file, args, options); } @@ -65,9 +80,14 @@ export class MockProcessService extends EventEmitter implements IProcessService public async shellExec(command: string, options?: ShellOptions): Promise> { let value: ExecutionResult | undefined; let valueReturned = false; - this.emit('shellExec', command, options, (result: ExecutionResult) => { value = result; valueReturned = true; }); + this.emit('shellExec', command, options, (result: ExecutionResult) => { + value = result; + valueReturned = true; + }); return valueReturned ? value! : this.procService.shellExec(command, options); } + // 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 3562b108eb9e..d290cae5bf71 100644 --- a/src/test/mocks/process.ts +++ b/src/test/mocks/process.ts @@ -1,29 +1,39 @@ // 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 }) { } - public on(event: string | symbol, listener: Function): this { + 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().object; + return createTypeMoq().object; } + + // eslint-disable-next-line class-methods-use-this public get stdin(): NodeJS.ReadStream { - return TypeMoq.Mock.ofType().object; + return createTypeMoq().object; } - public get execPath() : string { + // eslint-disable-next-line class-methods-use-this + public get execPath(): string { return ''; } } diff --git a/src/test/mocks/vsc/README.md b/src/test/mocks/vsc/README.md index 39fbe1508bbd..2803528e6276 100644 --- a/src/test/mocks/vsc/README.md +++ b/src/test/mocks/vsc/README.md @@ -1,6 +1,7 @@ # This folder contains classes exposed by VS Code required in running the unit tests. -* These classes are only used when running unit tests that are not hosted by VS Code. -* So even if these classes were buggy, it doesn't matter, running the tests under VS Code host will ensure the right classes are available. -* The purpose of these classes are to avoid having to use VS Code as the hosting environment for the tests, making it faster to run the tests and not have to rely on VS Code host to run the tests. -* Everyting in here must either be within a namespace prefixed with `vscMock` or exported types must be prefixed with `vscMock`. -This is to prevent developers from accidentally importing them into their Code. Even if they did, the extension would fail to load and tests would fail. + +- These classes are only used when running unit tests that are not hosted by VS Code. +- So even if these classes were buggy, it doesn't matter, running the tests under VS Code host will ensure the right classes are available. +- The purpose of these classes are to avoid having to use VS Code as the hosting environment for the tests, making it faster to run the tests and not have to rely on VS Code host to run the tests. +- Everything in here must either be within a namespace prefixed with `vscMock` or exported types must be prefixed with `vscMock`. + This is to prevent developers from accidentally importing them into their Code. Even if they did, the extension would fail to load and tests would fail. diff --git a/src/test/mocks/vsc/arrays.ts b/src/test/mocks/vsc/arrays.ts index bae8cc34b8b2..ad2020c57110 100644 --- a/src/test/mocks/vsc/arrays.ts +++ b/src/test/mocks/vsc/arrays.ts @@ -1,403 +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(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(array: T[], n: number = 0): T { - return array[array.length - (1 + n)]; +export function equals(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(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(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(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(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(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(data: T[], compare: (a: T, b: T) => number): T[] { + _divideAndMerge(data, compare); + return data; +} + +function _divideAndMerge(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)]; + } +} - /** - * 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(data: T[], compare: (a: T, b: T) => number): T[] { - _divideAndMerge(data, compare); - return data; +export function groupBy(data: T[], compare: (a: T, b: T) => number): T[][] { + const result: T[][] = []; + let currentGroup: T[] | undefined; + + 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(data: T[], compare: (a: T, b: T) => number): void { - if (data.length <= 1) { - // sorted +type IMutableSplice = { + deleteCount: number; + start: number; + toInsert: T[]; +}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ISplice = Array & any; + +/** + * Diffs two *sorted* arrays and computes the splices which apply the diff. + */ +export function sortedDiff(before: T[], after: T[], compare: (a: T, b: T) => number): ISplice[] { + const result: IMutableSplice[] = []; + + 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(data: T[], compare: (a: T, b: T) => number): T[][] { - const result: T[][] = []; - let currentGroup: T[]; - 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); - } + 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 = Array & any & { - deleteCount: number; + if (beforeIdx === before.length) { + pushSplice(beforeIdx, 0, after.slice(afterIdx)); + } else if (afterIdx === after.length) { + pushSplice(beforeIdx, before.length - beforeIdx, []); } - type ISplice = Array & any; - - /** - * Diffs two *sorted* arrays and computes the splices which apply the diff. - */ - export function sortedDiff(before: T[], after: T[], compare: (a: T, b: T) => number): ISplice[] { - const result: IMutableSplice[] = []; - - function pushSplice(start: number, deleteCount: number, toInsert: T[]): void { - if (deleteCount === 0 && toInsert.length === 0) { - return; - } - - const latest = result[result.length - 1]; - - if (latest && latest.start + latest.deleteCount === start) { - latest.deleteCount += deleteCount; - latest.toInsert.push(...toInsert); - } else { - result.push({ start, deleteCount, toInsert }); - } - } - let beforeIdx = 0; - let afterIdx = 0; - - while (true) { - if (beforeIdx === before.length) { - pushSplice(beforeIdx, 0, after.slice(afterIdx)); - break; - } - if (afterIdx === after.length) { - pushSplice(beforeIdx, before.length - beforeIdx, []); - break; - } - - const beforeElement = before[beforeIdx]; - const afterElement = after[afterIdx]; - const n = compare(beforeElement, afterElement); - if (n === 0) { - // equal - beforeIdx += 1; - afterIdx += 1; - } else if (n < 0) { - // beforeElement is smaller -> before element removed - pushSplice(beforeIdx, 1, []); - beforeIdx += 1; - } else if (n > 0) { - // beforeElement is greater -> after element added - pushSplice(beforeIdx, 0, [afterElement]); - afterIdx += 1; - } - } + return result; +} - 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(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(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(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(array: T[], compare: (a: T, b: T) => number, n: number): T[] { - if (n === 0) { - return []; +function topStep(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(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(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(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) || (>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) || (>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(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(array: T[], keyFn?: (t: T) => string): T[] { - if (!keyFn) { - return array.filter((element, position) => { - return array.indexOf(element) === position; - }); + const seen: Record = Object.create(null); + return array.filter((elem) => { + const key = keyFn(elem); + if (seen[key]) { + return false; } - const seen: { [key: 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(keyFn: (t: T) => string): (t: T) => boolean { + const seen: Record = Object.create(null); - export function uniqueFilter(keyFn: (t: T) => string): (t: T) => boolean { - const seen: { [key: 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(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(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(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; - } +export function commonPrefixLength(one: T[], other: T[], eqls: (a: T, b: T) => boolean = (a, b) => a === b): number { + let result = 0; - export function first(array: T[], fn: (item: T) => boolean, notFoundValue: T = null): T { - const index = firstIndex(array, fn); - return index < 0 ? notFoundValue : array[index]; + 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(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(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(arr: T[][]): T[] { - 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(num: number, valueFn: () => T, arr: T[] = []): T[] { - for (let i = 0; i < num; i++) { - arr[i] = valueFn(); - } + return result; +} - return arr; +export function fill(num: number, valueFn: () => T, arr: T[] = []): T[] { + for (let i = 0; i < num; i += 1) { + arr[i] = valueFn(); } - export function index(array: T[], indexer: (t: T) => string): { [key: string]: T; }; - export function index(array: T[], indexer: (t: T) => string, merger?: (t: T, r: R) => R): { [key: string]: R; }; - export function index(array: T[], indexer: (t: T) => string, merger: (t: T, r: R) => R = t => t as any): { [key: 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(array: T[], element: T): () => void { - array.push(element); - - return () => { - const index = array.indexOf(element); - if (index > -1) { - array.splice(index, 1); - } - }; - } +export function index(array: T[], indexer: (t: T) => string): Record; +export function index(array: T[], indexer: (t: T) => string, merger?: (t: T, r: R) => R): Record; +export function index( + array: T[], + indexer: (t: T) => string, + merger: (t: T, r: R) => R = (t) => (t as unknown) as R, +): Record { + 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(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(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(target: T[], insertIndex: number, insertArr: T[]): T[] { + const before = target.slice(0, insertIndex); + const after = target.slice(insertIndex); + return before.concat(insertArr, after); } diff --git a/src/test/mocks/vsc/charCode.ts b/src/test/mocks/vsc/charCode.ts new file mode 100644 index 000000000000..fe450d491ef1 --- /dev/null +++ b/src/test/mocks/vsc/charCode.ts @@ -0,0 +1,425 @@ +/* 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. + *--------------------------------------------------------------------------------------------*/ + +// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/ + +/** + * An inlined enum containing useful character codes (to be used with String.charCodeAt). + * Please leave the const keyword such that it gets inlined when compiled to JavaScript! + */ +export const enum CharCode { + Null = 0, + /** + * The `\b` character. + */ + Backspace = 8, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, + + U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent + U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent + U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent + U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde + U_Combining_Macron = 0x0304, // U+0304 Combining Macron + U_Combining_Overline = 0x0305, // U+0305 Combining Overline + U_Combining_Breve = 0x0306, // U+0306 Combining Breve + U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above + U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis + U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above + U_Combining_Ring_Above = 0x030a, // U+030A Combining Ring Above + U_Combining_Double_Acute_Accent = 0x030b, // U+030B Combining Double Acute Accent + U_Combining_Caron = 0x030c, // U+030C Combining Caron + U_Combining_Vertical_Line_Above = 0x030d, // U+030D Combining Vertical Line Above + U_Combining_Double_Vertical_Line_Above = 0x030e, // U+030E Combining Double Vertical Line Above + U_Combining_Double_Grave_Accent = 0x030f, // U+030F Combining Double Grave Accent + U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu + U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve + U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above + U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above + U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above + U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right + U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below + U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below + U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below + U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below + U_Combining_Left_Angle_Above = 0x031a, // U+031A Combining Left Angle Above + U_Combining_Horn = 0x031b, // U+031B Combining Horn + U_Combining_Left_Half_Ring_Below = 0x031c, // U+031C Combining Left Half Ring Below + U_Combining_Up_Tack_Below = 0x031d, // U+031D Combining Up Tack Below + U_Combining_Down_Tack_Below = 0x031e, // U+031E Combining Down Tack Below + U_Combining_Plus_Sign_Below = 0x031f, // U+031F Combining Plus Sign Below + U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below + U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below + U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below + U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below + U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below + U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below + U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below + U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla + U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek + U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below + U_Combining_Bridge_Below = 0x032a, // U+032A Combining Bridge Below + U_Combining_Inverted_Double_Arch_Below = 0x032b, // U+032B Combining Inverted Double Arch Below + U_Combining_Caron_Below = 0x032c, // U+032C Combining Caron Below + U_Combining_Circumflex_Accent_Below = 0x032d, // U+032D Combining Circumflex Accent Below + U_Combining_Breve_Below = 0x032e, // U+032E Combining Breve Below + U_Combining_Inverted_Breve_Below = 0x032f, // U+032F Combining Inverted Breve Below + U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below + U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below + U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line + U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line + U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay + U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay + U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay + U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay + U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay + U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below + U_Combining_Inverted_Bridge_Below = 0x033a, // U+033A Combining Inverted Bridge Below + U_Combining_Square_Below = 0x033b, // U+033B Combining Square Below + U_Combining_Seagull_Below = 0x033c, // U+033C Combining Seagull Below + U_Combining_X_Above = 0x033d, // U+033D Combining X Above + U_Combining_Vertical_Tilde = 0x033e, // U+033E Combining Vertical Tilde + U_Combining_Double_Overline = 0x033f, // U+033F Combining Double Overline + U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark + U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark + U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni + U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis + U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos + U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni + U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above + U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below + U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below + U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below + U_Combining_Not_Tilde_Above = 0x034a, // U+034A Combining Not Tilde Above + U_Combining_Homothetic_Above = 0x034b, // U+034B Combining Homothetic Above + U_Combining_Almost_Equal_To_Above = 0x034c, // U+034C Combining Almost Equal To Above + U_Combining_Left_Right_Arrow_Below = 0x034d, // U+034D Combining Left Right Arrow Below + U_Combining_Upwards_Arrow_Below = 0x034e, // U+034E Combining Upwards Arrow Below + U_Combining_Grapheme_Joiner = 0x034f, // U+034F Combining Grapheme Joiner + U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above + U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above + U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata + U_Combining_X_Below = 0x0353, // U+0353 Combining X Below + U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below + U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below + U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below + U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above + U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right + U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below + U_Combining_Double_Ring_Below = 0x035a, // U+035A Combining Double Ring Below + U_Combining_Zigzag_Above = 0x035b, // U+035B Combining Zigzag Above + U_Combining_Double_Breve_Below = 0x035c, // U+035C Combining Double Breve Below + U_Combining_Double_Breve = 0x035d, // U+035D Combining Double Breve + U_Combining_Double_Macron = 0x035e, // U+035E Combining Double Macron + U_Combining_Double_Macron_Below = 0x035f, // U+035F Combining Double Macron Below + U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde + U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve + U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below + U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A + U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E + U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I + U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O + U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U + U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C + U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D + U_Combining_Latin_Small_Letter_H = 0x036a, // U+036A Combining Latin Small Letter H + U_Combining_Latin_Small_Letter_M = 0x036b, // U+036B Combining Latin Small Letter M + U_Combining_Latin_Small_Letter_R = 0x036c, // U+036C Combining Latin Small Letter R + U_Combining_Latin_Small_Letter_T = 0x036d, // U+036D Combining Latin Small Letter T + U_Combining_Latin_Small_Letter_V = 0x036e, // U+036E Combining Latin Small Letter V + U_Combining_Latin_Small_Letter_X = 0x036f, // U+036F Combining Latin Small Letter X + + /** + * Unicode Character 'LINE SEPARATOR' (U+2028) + * http://www.fileformat.info/info/unicode/char/2028/index.htm + */ + LINE_SEPARATOR_2028 = 8232, + + // http://www.fileformat.info/info/unicode/category/Sk/list.htm + U_CIRCUMFLEX = 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 + U_CEDILLA = 0x00b8, // U+00B8 CEDILLA + U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02c2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD + U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02c3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD + U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02c4, // U+02C4 MODIFIER LETTER UP ARROWHEAD + U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02c5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD + U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02d2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING + U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02d3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING + U_MODIFIER_LETTER_UP_TACK = 0x02d4, // U+02D4 MODIFIER LETTER UP TACK + U_MODIFIER_LETTER_DOWN_TACK = 0x02d5, // U+02D5 MODIFIER LETTER DOWN TACK + U_MODIFIER_LETTER_PLUS_SIGN = 0x02d6, // U+02D6 MODIFIER LETTER PLUS SIGN + U_MODIFIER_LETTER_MINUS_SIGN = 0x02d7, // U+02D7 MODIFIER LETTER MINUS SIGN + U_BREVE = 0x02d8, // U+02D8 BREVE + U_DOT_ABOVE = 0x02d9, // U+02D9 DOT ABOVE + U_RING_ABOVE = 0x02da, // U+02DA RING ABOVE + U_OGONEK = 0x02db, // U+02DB OGONEK + U_SMALL_TILDE = 0x02dc, // U+02DC SMALL TILDE + U_DOUBLE_ACUTE_ACCENT = 0x02dd, // U+02DD DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02de, // U+02DE MODIFIER LETTER RHOTIC HOOK + U_MODIFIER_LETTER_CROSS_ACCENT = 0x02df, // U+02DF MODIFIER LETTER CROSS ACCENT + U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02e5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR + U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02e6, // U+02E6 MODIFIER LETTER HIGH TONE BAR + U_MODIFIER_LETTER_MID_TONE_BAR = 0x02e7, // U+02E7 MODIFIER LETTER MID TONE BAR + U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02e8, // U+02E8 MODIFIER LETTER LOW TONE BAR + U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02e9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR + U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02ea, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK + U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02eb, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK + U_MODIFIER_LETTER_UNASPIRATED = 0x02ed, // U+02ED MODIFIER LETTER UNASPIRATED + U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02ef, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD + U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02f0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD + U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02f1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD + U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02f2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD + U_MODIFIER_LETTER_LOW_RING = 0x02f3, // U+02F3 MODIFIER LETTER LOW RING + U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02f4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02f5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02f6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_LOW_TILDE = 0x02f7, // U+02F7 MODIFIER LETTER LOW TILDE + U_MODIFIER_LETTER_RAISED_COLON = 0x02f8, // U+02F8 MODIFIER LETTER RAISED COLON + U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02f9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE + U_MODIFIER_LETTER_END_HIGH_TONE = 0x02fa, // U+02FA MODIFIER LETTER END HIGH TONE + U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02fb, // U+02FB MODIFIER LETTER BEGIN LOW TONE + U_MODIFIER_LETTER_END_LOW_TONE = 0x02fc, // U+02FC MODIFIER LETTER END LOW TONE + U_MODIFIER_LETTER_SHELF = 0x02fd, // U+02FD MODIFIER LETTER SHELF + U_MODIFIER_LETTER_OPEN_SHELF = 0x02fe, // U+02FE MODIFIER LETTER OPEN SHELF + U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02ff, // U+02FF MODIFIER LETTER LOW LEFT ARROW + U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN + U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS + U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS + U_GREEK_KORONIS = 0x1fbd, // U+1FBD GREEK KORONIS + U_GREEK_PSILI = 0x1fbf, // U+1FBF GREEK PSILI + U_GREEK_PERISPOMENI = 0x1fc0, // U+1FC0 GREEK PERISPOMENI + U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1fc1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI + U_GREEK_PSILI_AND_VARIA = 0x1fcd, // U+1FCD GREEK PSILI AND VARIA + U_GREEK_PSILI_AND_OXIA = 0x1fce, // U+1FCE GREEK PSILI AND OXIA + U_GREEK_PSILI_AND_PERISPOMENI = 0x1fcf, // U+1FCF GREEK PSILI AND PERISPOMENI + U_GREEK_DASIA_AND_VARIA = 0x1fdd, // U+1FDD GREEK DASIA AND VARIA + U_GREEK_DASIA_AND_OXIA = 0x1fde, // U+1FDE GREEK DASIA AND OXIA + U_GREEK_DASIA_AND_PERISPOMENI = 0x1fdf, // U+1FDF GREEK DASIA AND PERISPOMENI + U_GREEK_DIALYTIKA_AND_VARIA = 0x1fed, // U+1FED GREEK DIALYTIKA AND VARIA + U_GREEK_DIALYTIKA_AND_OXIA = 0x1fee, // U+1FEE GREEK DIALYTIKA AND OXIA + U_GREEK_VARIA = 0x1fef, // U+1FEF GREEK VARIA + U_GREEK_OXIA = 0x1ffd, // U+1FFD GREEK OXIA + U_GREEK_DASIA = 0x1ffe, // U+1FFE GREEK DASIA + + U_OVERLINE = 0x203e, // Unicode Character 'OVERLINE' + + /** + * UTF-8 BOM + * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) + * http://www.fileformat.info/info/unicode/char/feff/index.htm + */ + UTF8_BOM = 65279, +} diff --git a/src/test/mocks/vsc/extHostedTypes.ts b/src/test/mocks/vsc/extHostedTypes.ts index c8f5a97814a5..c2c1188c3449 100644 --- a/src/test/mocks/vsc/extHostedTypes.ts +++ b/src/test/mocks/vsc/extHostedTypes.ts @@ -1,1939 +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'; - -export namespace vscMockExtHostedTypes { +import * as vscMockHtmlContent from './htmlContent'; +import * as vscMockStrings from './strings'; +import * as vscUri from './uri'; +import { generateUuid } from './uuid'; + +export enum NotebookCellKind { + Markup = 1, + Code = 2, +} - export interface IRelativePattern { - base: string; - pattern: string; - pathToRelative(from: string, to: string): string; - } +export enum CellOutputKind { + Text = 1, + Error = 2, + Rich = 3, +} +export enum NotebookCellRunState { + Running = 1, + Idle = 2, + Success = 3, + Error = 4, +} - // tslint:disable:all - const illegalArgument = (msg = 'Illegal Argument') => new Error(msg); +export interface IRelativePattern { + base: string; + pattern: string; + pathToRelative(from: string, to: string): string; +} - export class Disposable { +const illegalArgument = (msg = 'Illegal Argument') => new Error(msg); - 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(); } - 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(); - this._callOnDispose = undefined; - } - } + constructor(callOnDispose: () => void) { + this._callOnDispose = callOnDispose; } - export class Position { + dispose(): void { + if (typeof this._callOnDispose === 'function') { + this._callOnDispose(); + this._callOnDispose = undefined; + } + } +} - static Min(...positions: Position[]): Position { - let result = positions.pop(); - for (let p of positions) { - 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; } - return result; } + return result || new Position(0, 0); + } - static Max(...positions: Position[]): Position { - let result = positions.pop(); - for (let p of positions) { - 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; } - 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 } = 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 } = 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 line(): number { + return this._line; + } - get character(): number { - return this._character; + get character(): number { + return this._character; + } + + constructor(line: number, character: number) { + if (line < 0) { + throw illegalArgument('line must be non-negative'); + } + if (character < 0) { + throw illegalArgument('character must be non-negative'); } + this._line = line; + this._character = character; + } - constructor(line: number, character: number) { - if (line < 0) { - throw illegalArgument('line must be non-negative'); - } - if (character < 0) { - throw illegalArgument('character must be non-negative'); - } - this._line = line; - this._character = character; + isBefore(other: Position): boolean { + if (this._line < other._line) { + return true; } + if (other._line < this._line) { + return false; + } + return this._character < other._character; + } - isBefore(other: Position): boolean { - if (this._line < other._line) { - return true; - } - if (other._line < this._line) { - return false; - } - return this._character < other._character; + isBeforeOrEqual(other: Position): boolean { + if (this._line < other._line) { + return true; + } + if (other._line < this._line) { + return false; } + return this._character <= other._character; + } - isBeforeOrEqual(other: Position): boolean { - if (this._line < other._line) { - return true; - } - if (other._line < this._line) { - return false; - } - return this._character <= other._character; + isAfter(other: Position): boolean { + return !this.isBeforeOrEqual(other); + } + + isAfterOrEqual(other: Position): boolean { + return !this.isBefore(other); + } + + isEqual(other: Position): boolean { + return this._line === other._line && this._character === other._character; + } + + compareTo(other: Position): number { + if (this._line < other._line) { + return -1; } + if (this._line > other.line) { + return 1; + } + // equal line + if (this._character < other._character) { + return -1; + } + if (this._character > other._character) { + return 1; + } + // equal line and character + return 0; + } - isAfter(other: Position): boolean { - return !this.isBeforeOrEqual(other); + translate(change: { lineDelta?: number; characterDelta?: number }): Position; + + translate(lineDelta?: number, characterDelta?: number): Position; + + translate( + lineDeltaOrChange: number | { lineDelta?: number; characterDelta?: number } | undefined, + characterDelta = 0, + ): Position { + if (lineDeltaOrChange === null || characterDelta === null) { + throw illegalArgument(); } - isAfterOrEqual(other: Position): boolean { - return !this.isBefore(other); + 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: Position): boolean { - return this._line === other._line && this._character === other._character; + if (lineDelta === 0 && characterDelta === 0) { + return this; } + return new Position(this.line + lineDelta, this.character + characterDelta); + } - 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; - } - } + with(change: { line?: number; character?: number }): Position; + + with(line?: number, character?: number): Position; + + with( + lineOrChange: number | { line?: number; character?: number } | undefined, + character: number = this.character, + ): Position { + if (lineOrChange === null || character === null) { + throw illegalArgument(); } - translate(change: { lineDelta?: number; characterDelta?: number; }): Position; - translate(lineDelta?: number, characterDelta?: number): Position; - translate(lineDeltaOrChange: number | { lineDelta?: number; characterDelta?: number; }, characterDelta: number = 0): Position { + let line: number; + if (typeof lineOrChange === 'undefined') { + line = this.line; + } else if (typeof lineOrChange === 'number') { + line = lineOrChange; + } else { + line = typeof lineOrChange.line === 'number' ? lineOrChange.line : this.line; + character = typeof lineOrChange.character === 'number' ? lineOrChange.character : this.character; + } - if (lineDeltaOrChange === null || characterDelta === null) { - throw illegalArgument(); - } + if (line === this.line && character === this.character) { + return this; + } + return new Position(line, character); + } - 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; - } + toJSON(): { line: number; character: number } { + return { line: this.line, character: this.character }; + } +} - if (lineDelta === 0 && characterDelta === 0) { - return this; - } - return new Position(this.line + lineDelta, this.character + characterDelta); +export class Range { + static isRange(thing: unknown): thing is vscode.Range { + if (thing instanceof Range) { + return true; + } + if (!thing) { + return false; } + return Position.isPosition((thing as Range).start) && Position.isPosition((thing as Range).end); + } - with(change: { line?: number; character?: number; }): Position; - with(line?: number, character?: number): Position; - with(lineOrChange: number | { line?: number; character?: number; }, character: number = this.character): Position { + protected _start: Position; - if (lineOrChange === null || character === null) { - throw illegalArgument(); - } + protected _end: Position; - let line: number; - if (typeof lineOrChange === 'undefined') { - line = this.line; + get start(): Position { + return this._start; + } - } else if (typeof lineOrChange === 'number') { - line = lineOrChange; + get end(): Position { + return this._end; + } - } else { - line = typeof lineOrChange.line === 'number' ? lineOrChange.line : this.line; - character = typeof lineOrChange.character === 'number' ? lineOrChange.character : this.character; - } + constructor(start: Position, end: Position); - if (line === this.line && character === this.character) { - return this; - } - return new Position(line, character); + constructor(startLine: number, startColumn: number, endLine: number, endColumn: number); + + constructor( + startLineOrStart: number | Position, + startColumnOrEnd: number | Position, + endLine?: number, + endColumn?: number, + ) { + let start: Position | undefined; + let end: Position | undefined; + + 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; } - toJSON(): any { - return { line: this.line, character: this.character }; + if (!start || !end) { + throw new Error('Invalid arguments'); } - } - export class Range { + if (start.isBefore(end)) { + this._start = start; + this._end = end; + } else { + this._start = end; + this._end = start; + } + } - static isRange(thing: any): thing is vscode.Range { - if (thing instanceof Range) { - 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 Position.isPosition((thing).start) - && Position.isPosition((thing.end)); + return true; } + return false; + } - protected _start: Position; - protected _end: Position; + isEqual(other: Range): boolean { + return this._start.isEqual(other._start) && this._end.isEqual(other._end); + } - get start(): Position { - return this._start; + 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); + } - get end(): Position { - return this._end; + union(other: Range): Range { + if (this.contains(other)) { + return this; } + if (other.contains(this)) { + return other; + } + const start = Position.Min(other.start, this._start); + const end = Position.Max(other.end, this.end); + return new Range(start, end); + } - constructor(start: Position, end: Position); - constructor(startLine: number, startColumn: number, endLine: number, endColumn: number); - constructor(startLineOrStart: number | Position, startColumnOrEnd: number | Position, endLine?: number, endColumn?: number) { - let start: Position; - let end: Position; - - if (typeof startLineOrStart === 'number' && typeof startColumnOrEnd === 'number' && typeof endLine === 'number' && typeof endColumn === 'number') { - start = new Position(startLineOrStart, startColumnOrEnd); - end = new Position(endLine, endColumn); - } else if (startLineOrStart instanceof Position && startColumnOrEnd instanceof Position) { - start = startLineOrStart; - end = startColumnOrEnd; - } + get isEmpty(): boolean { + return this._start.isEqual(this._end); + } - if (!start || !end) { - throw new Error('Invalid arguments'); - } + get isSingleLine(): boolean { + return this._start.line === this._end.line; + } - if (start.isBefore(end)) { - this._start = start; - this._end = end; - } else { - this._start = end; - this._end = start; - } - } + with(change: { start?: Position; end?: Position }): Range; - contains(positionOrRange: Position | Range): boolean { - if (positionOrRange instanceof Range) { - return this.contains(positionOrRange._start) - && this.contains(positionOrRange._end); + with(start?: Position, end?: Position): Range; - } else if (positionOrRange instanceof Position) { - if (positionOrRange.isBefore(this._start)) { - return false; - } - if (this._end.isBefore(positionOrRange)) { - return false; - } - return true; - } - return false; + with(startOrChange: Position | { start?: Position; end?: Position } | undefined, end: Position = this.end): Range { + if (startOrChange === null || end === null) { + throw illegalArgument(); } - isEqual(other: Range): boolean { - return this._start.isEqual(other._start) && this._end.isEqual(other._end); + 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; } - 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: - // |-----| - // |----| - return undefined; - } - return new Range(start, end); + if (start.isEqual(this._start) && end.isEqual(this.end)) { + return this; } + return new Range(start, end); + } - union(other: Range): Range { - if (this.contains(other)) { - return this; - } else if (other.contains(this)) { - return other; - } - let start = Position.Min(other.start, this._start); - let end = Position.Max(other.end, this.end); - return new Range(start, end); - } + toJSON(): [Position, Position] { + return [this.start, this.end]; + } +} - get isEmpty(): boolean { - return this._start.isEqual(this._end); +export class Selection extends Range { + static isSelection(thing: unknown): thing is Selection { + if (thing instanceof Selection) { + return true; } - - get isSingleLine(): boolean { - return this._start.line === this._end.line; + if (!thing) { + return false; } + return ( + Range.isRange(thing) && + Position.isPosition((thing).anchor) && + Position.isPosition((thing).active) && + typeof (thing).isReversed === 'boolean' + ); + } - with(change: { start?: Position, end?: Position }): Range; - with(start?: Position, end?: Position): Range; - with(startOrChange: Position | { start?: Position, end?: Position }, end: Position = this.end): Range { + private _anchor: Position; - if (startOrChange === null || end === null) { - throw illegalArgument(); - } + public get anchor(): Position { + return this._anchor; + } - let start: Position; - if (!startOrChange) { - start = this.start; + private _active: Position; - } else if (Position.isPosition(startOrChange)) { - start = startOrChange; + public get active(): Position { + return this._active; + } - } else { - start = startOrChange.start || this.start; - end = startOrChange.end || this.end; - } + constructor(anchor: Position, active: Position); - if (start.isEqual(this._start) && end.isEqual(this.end)) { - return this; - } - return new Range(start, end); + constructor(anchorLine: number, anchorColumn: number, activeLine: number, activeColumn: number); + + constructor( + anchorLineOrAnchor: number | Position, + anchorColumnOrActive: number | Position, + activeLine?: number, + activeColumn?: number, + ) { + let anchor: Position | undefined; + let active: Position | undefined; + + 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; } - toJSON(): any { - return [this.start, this.end]; + if (!anchor || !active) { + throw new Error('Invalid arguments'); } + + super(anchor, active); + + this._anchor = anchor; + this._active = active; } - export class Selection extends Range { + get isReversed(): boolean { + return this._anchor === this._end; + } - static isSelection(thing: any): thing is Selection { - if (thing instanceof Selection) { - return true; - } - if (!thing) { - return false; - } - return Range.isRange(thing) - && Position.isPosition((thing).anchor) - && Position.isPosition((thing).active) - && typeof (thing).isReversed === 'boolean'; - } + toJSON(): [Position, Position] { + return ({ + start: this.start, + end: this.end, + active: this.active, + anchor: this.anchor, + } as unknown) as [Position, Position]; + } +} - private _anchor: Position; +export enum EndOfLine { + LF = 1, + CRLF = 2, +} - public get anchor(): Position { - return this._anchor; +export class TextEdit { + static isTextEdit(thing: unknown): thing is TextEdit { + if (thing instanceof TextEdit) { + return true; + } + if (!thing) { + return false; } + return Range.isRange(thing) && typeof (thing).newText === 'string'; + } - private _active: Position; + static replace(range: Range, newText: string): TextEdit { + return new TextEdit(range, newText); + } - public get active(): Position { - return this._active; - } + static insert(position: Position, newText: string): TextEdit { + return TextEdit.replace(new Range(position, position), newText); + } - 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; + static delete(range: Range): TextEdit { + return TextEdit.replace(range, ''); + } - 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; - } + static setEndOfLine(eol: EndOfLine): TextEdit { + const ret = new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), ''); + ret.newEol = eol; + return ret; + } - if (!anchor || !active) { - throw new Error('Invalid arguments'); - } + _range: Range = new Range(new Position(0, 0), new Position(0, 0)); - super(anchor, active); + newText = ''; - this._anchor = anchor; - this._active = active; - } + _newEol: EndOfLine = EndOfLine.LF; - get isReversed(): boolean { - return this._anchor === this._end; + get range(): Range { + return this._range; + } + + set range(value: Range) { + if (value && !Range.isRange(value)) { + throw illegalArgument('range'); } + this._range = value; + } - toJSON() { - return { - start: this.start, - end: this.end, - active: this.active, - anchor: this.anchor - }; + get newEol(): EndOfLine { + return this._newEol; + } + + set newEol(value: EndOfLine) { + if (value && typeof value !== 'number') { + throw illegalArgument('newEol'); } + this._newEol = value; } - export enum EndOfLine { - LF = 1, - CRLF = 2 + constructor(range: Range, newText: string) { + this.range = range; + this.newText = newText; } +} - export class TextEdit { +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. + } - static isTextEdit(thing: any): thing is TextEdit { - if (thing instanceof TextEdit) { - return true; - } - if (!thing) { - return false; - } - return Range.isRange((thing)) - && typeof (thing).newText === 'string'; - } + // eslint-disable-next-line class-methods-use-this + replaceNotebookCellOutputItems( + _uri: vscode.Uri, + _index: number, + _outputId: string, + _items: vscode.NotebookCellOutputItem[], + _metadata?: vscode.WorkspaceEditEntryMetadata, + ): void { + // Noop. + } - static replace(range: Range, newText: string): TextEdit { - return new TextEdit(range, newText); - } + // eslint-disable-next-line class-methods-use-this + appendNotebookCellOutputItems( + _uri: vscode.Uri, + _index: number, + _outputId: string, + _items: vscode.NotebookCellOutputItem[], + _metadata?: vscode.WorkspaceEditEntryMetadata, + ): void { + // Noop. + } - static insert(position: Position, newText: string): TextEdit { - return TextEdit.replace(new Range(position, position), newText); - } + // eslint-disable-next-line class-methods-use-this + replaceNotebookCells( + _uri: vscode.Uri, + _start: number, + _end: number, + _cells: vscode.NotebookCellData[], + _metadata?: vscode.WorkspaceEditEntryMetadata, + ): void { + // Noop. + } - static delete(range: Range): TextEdit { - return TextEdit.replace(range, ''); - } + // eslint-disable-next-line class-methods-use-this + replaceNotebookCellOutput( + _uri: vscode.Uri, + _index: number, + _outputs: vscode.NotebookCellOutput[], + _metadata?: vscode.WorkspaceEditEntryMetadata, + ): void { + // Noop. + } - static setEndOfLine(eol: EndOfLine): TextEdit { - let ret = new TextEdit(undefined, undefined); - ret.newEol = eol; - return ret; - } + private _seqPool = 0; - protected _range: Range; - protected _newText: string; - protected _newEol: EndOfLine; + private _resourceEdits: { seq: number; from: vscUri.URI; to: vscUri.URI }[] = []; - get range(): Range { - return this._range; - } + private _textEdits = new Map(); - set range(value: Range) { - if (value && !Range.isRange(value)) { - throw illegalArgument('range'); - } - this._range = value; - } + // createResource(uri: vscode.Uri): void { + // this.renameResource(undefined, uri); + // } - get newText(): string { - return this._newText || ''; - } + // deleteResource(uri: vscode.Uri): void { + // this.renameResource(uri, undefined); + // } - set newText(value: string) { - if (value && typeof value !== 'string') { - throw illegalArgument('newText'); - } - this._newText = value; - } + // renameResource(from: vscode.Uri, to: vscode.Uri): void { + // this._resourceEdits.push({ seq: this._seqPool+= 1, from, to }); + // } - get newEol(): EndOfLine { - return this._newEol; - } + // resourceEdits(): [vscode.Uri, vscode.Uri][] { + // return this._resourceEdits.map(({ from, to }) => (<[vscode.Uri, vscode.Uri]>[from, to])); + // } - set newEol(value: EndOfLine) { - if (value && typeof value !== 'number') { - throw illegalArgument('newEol'); - } - this._newEol = value; - } + // eslint-disable-next-line class-methods-use-this + createFile(_uri: vscode.Uri, _options?: { overwrite?: boolean; ignoreIfExists?: boolean }): void { + throw new Error('Method not implemented.'); + } - constructor(range: Range, newText: string) { - this.range = range; - this.newText = newText; - } + // eslint-disable-next-line class-methods-use-this + deleteFile(_uri: vscode.Uri, _options?: { recursive?: boolean; ignoreIfNotExists?: boolean }): void { + throw new Error('Method not implemented.'); + } - toJSON(): any { - return { - range: this.range, - newText: this.newText, - newEol: this._newEol - }; + // 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.'); + } + + 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); } - export class WorkspaceEdit implements vscode.WorkspaceEdit { + insert(resource: vscUri.URI, position: Position, newText: string): void { + this.replace(resource, new Range(position, position), newText); + } - private _seqPool: number = 0; + delete(resource: vscUri.URI, range: Range): void { + this.replace(resource, range, ''); + } - private _resourceEdits: { seq: number, from: vscUri.URI, to: vscUri.URI }[] = []; - private _textEdits = new Map(); + has(uri: vscUri.URI): boolean { + return this._textEdits.has(uri.toString()); + } - // createResource(uri: vscode.Uri): void { - // this.renameResource(undefined, uri); - // } + 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); + } + if (!edits) { + data.edits = []; + } else { + data.edits = edits.slice(0) as TextEdit[]; + } + } - // deleteResource(uri: vscode.Uri): void { - // this.renameResource(uri, undefined); - // } + get(uri: vscUri.URI): TextEdit[] { + if (!this._textEdits.has(uri.toString())) { + return []; + } + const { edits } = this._textEdits.get(uri.toString()) || {}; + return edits ? edits.slice() : []; + } - // renameResource(from: vscode.Uri, to: vscode.Uri): void { - // this._resourceEdits.push({ seq: this._seqPool++, from, to }); - // } + entries(): [vscUri.URI, TextEdit[]][] { + const res: [vscUri.URI, TextEdit[]][] = []; + this._textEdits.forEach((value) => res.push([value.uri, value.edits])); + return res.slice(); + } - // resourceEdits(): [vscode.Uri, vscode.Uri][] { - // return this._resourceEdits.map(({ from, to }) => (<[vscode.Uri, vscode.Uri]>[from, to])); - // } + 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; + } - 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."); + get size(): number { + return this._textEdits.size + this._resourceEdits.length; + } + + toJSON(): [vscUri.URI, TextEdit[]][] { + return this.entries(); + } +} + +export class SnippetString { + static isSnippetString(thing: unknown): thing is SnippetString { + if (thing instanceof SnippetString) { + return true; } - renameFile(oldUri: vscode.Uri, newUri: vscode.Uri, options?: { overwrite?: boolean; ignoreIfExists?: boolean; }): void { - throw new Error("Method not implemented."); + if (!thing) { + return false; } + return typeof (thing).value === 'string'; + } - 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); - } + private static _escape(value: string): string { + return value.replace(/\$|}|\\/g, '\\$&'); + } - insert(resource: vscUri.URI, position: Position, newText: string): void { - this.replace(resource, new Range(position, position), newText); - } + private _tabstop = 1; - delete(resource: vscUri.URI, range: Range): void { - this.replace(resource, range, ''); - } + value: string; - has(uri: vscUri.URI): boolean { - return this._textEdits.has(uri.toString()); - } + constructor(value?: string) { + this.value = value || ''; + } - 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) { - data.edits = undefined; - } else { - data.edits = edits.slice(0); - } - } + appendText(string: string): SnippetString { + this.value += SnippetString._escape(string); + return this; + } - get(uri: vscUri.URI): TextEdit[] { - if (!this._textEdits.has(uri.toString())) { - return undefined; - } - const { edits } = this._textEdits.get(uri.toString()); - return edits ? edits.slice() : undefined; - } + appendTabstop(number: number = (this._tabstop += 1)): SnippetString { + this.value += '$'; + this.value += number; + return this; + } - entries(): [vscUri.URI, TextEdit[]][] { - const res: [vscUri.URI, TextEdit[]][] = []; - this._textEdits.forEach(value => res.push([value.uri, value.edits])); - return res.slice(); + 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); } - 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; - } + this.value += '${'; + this.value += number; + this.value += ':'; + this.value += value; + this.value += '}'; + + return this; + } + + 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 += '|}'; - get size(): number { - return this._textEdits.size + this._resourceEdits.length; + 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) } - toJSON(): any { - return this.entries(); + this.value += '${'; + this.value += name; + if (defaultValue) { + this.value += ':'; + this.value += defaultValue; } + this.value += '}'; + + return this; } +} - export class SnippetString { +export enum DiagnosticTag { + Unnecessary = 1, +} - static isSnippetString(thing: any): thing is SnippetString { - if (thing instanceof SnippetString) { - return true; - } - if (!thing) { - return false; - } - return typeof (thing).value === 'string'; - } +export enum DiagnosticSeverity { + Hint = 3, + Information = 2, + Warning = 1, + Error = 0, +} - private static _escape(value: string): string { - return value.replace(/\$|}|\\/g, '\\$&'); +export class Location { + static isLocation(thing: unknown): thing is Location { + if (thing instanceof Location) { + return true; } + if (!thing) { + return false; + } + return Range.isRange((thing).range) && vscUri.URI.isUri((thing).uri); + } - private _tabstop: number = 1; + uri: vscUri.URI; - value: string; + range: Range = new Range(new Position(0, 0), new Position(0, 0)); - constructor(value?: string) { - this.value = value || ''; - } + constructor(uri: vscUri.URI, rangeOrPosition: Range | Position) { + this.uri = uri; - appendText(string: string): SnippetString { - this.value += SnippetString._escape(string); - return this; + 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'); } + } - appendTabstop(number: number = this._tabstop++): SnippetString { - this.value += '$'; - this.value += number; - return this; + toJSON(): { uri: vscUri.URI; range: Range } { + return { + uri: this.uri, + range: this.range, + }; + } +} + +export class DiagnosticRelatedInformation { + static is(thing: unknown): thing is DiagnosticRelatedInformation { + if (!thing) { + return false; } + return ( + typeof (thing).message === 'string' && + (thing).location && + Range.isRange((thing).location.range) && + vscUri.URI.isUri((thing).location.uri) + ); + } - appendPlaceholder(value: string | ((snippet: SnippetString) => any), number: number = this._tabstop++): SnippetString { + location: Location; - 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); - } + message: string; - this.value += '${'; - this.value += number; - this.value += ':'; - this.value += value; - this.value += '}'; + constructor(location: Location, message: string) { + this.location = location; + this.message = message; + } +} - return this; - } +export class Diagnostic { + range: Range; - appendVariable(name: string, defaultValue?: string | ((snippet: SnippetString) => any)): SnippetString { + message: string; - if (typeof defaultValue === 'function') { - const nested = new SnippetString(); - nested._tabstop = this._tabstop; - defaultValue(nested); - this._tabstop = nested._tabstop; - defaultValue = nested.value; + source = ''; - } else if (typeof defaultValue === 'string') { - defaultValue = defaultValue.replace(/\$|}/g, '\\$&'); - } + code: string | number = ''; - this.value += '${'; - this.value += name; - if (defaultValue) { - this.value += ':'; - this.value += defaultValue; - } - this.value += '}'; + severity: DiagnosticSeverity; + relatedInformation: DiagnosticRelatedInformation[] = []; - return this; + 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 = 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 enum DiagnosticTag { - Unnecessary = 1, +export class DocumentHighlight { + range: Range; + + kind: DocumentHighlightKind; + + constructor(range: Range, kind: DocumentHighlightKind = DocumentHighlightKind.Text) { + this.range = range; + this.kind = kind; } - export enum DiagnosticSeverity { - Hint = 3, - Information = 2, - Warning = 1, - Error = 0 + toJSON(): { range: Range; kind: DocumentHighlightKind } { + return { + range: this.range, + kind: (DocumentHighlightKind[this.kind] as unknown) as DocumentHighlightKind, + }; } +} - export class Location { +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, +} - static isLocation(thing: any): thing is Location { - if (thing instanceof Location) { - return true; - } - if (!thing) { - return false; - } - return Range.isRange((thing).range) - && vscUri.URI.isUri((thing).uri); - } +export class SymbolInformation { + name: string; - uri: vscUri.URI; - range: Range; + location: Location = new Location( + vscUri.URI.parse('testLocation'), + new Range(new Position(0, 0), new Position(0, 0)), + ); - constructor(uri: vscUri.URI, rangeOrPosition: Range | Position) { - this.uri = uri; + kind: SymbolKind; - 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'); - } + 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; } - toJSON(): any { - return { - uri: this.uri, - range: this.range - }; + if (locationOrUri instanceof Location) { + this.location = locationOrUri; + } else if (rangeOrContainer instanceof Range) { + this.location = new Location(locationOrUri as vscUri.URI, rangeOrContainer); } } - export class DiagnosticRelatedInformation { + 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; - static is(thing: any): thing is DiagnosticRelatedInformation { - if (!thing) { - return false; - } - return typeof (thing).message === 'string' - && (thing).location - && Range.isRange((thing).location.range) - && vscUri.URI.isUri((thing).location.uri); - } + children: SymbolInformation2[]; - location: Location; - message: string; + constructor(name: string, kind: SymbolKind, containerName: string, location: Location) { + super(name, kind, containerName, location); - constructor(location: Location, message: string) { - this.location = location; - this.message = message; - } + this.children = []; + this.definingRange = location.range; } +} - export class Diagnostic { +export enum CodeActionTrigger { + Automatic = 1, + Manual = 2, +} - range: Range; - message: string; - source: string; - code: string | number; - severity: DiagnosticSeverity; - relatedInformation: DiagnosticRelatedInformation[]; - customTags?: DiagnosticTag[]; +export class CodeAction { + title: string; - constructor(range: Range, message: string, severity: DiagnosticSeverity = DiagnosticSeverity.Error) { - this.range = range; - this.message = message; - this.severity = severity; - } + command?: vscode.Command; - toJSON(): any { - return { - severity: DiagnosticSeverity[this.severity], - message: this.message, - range: this.range, - source: this.source, - code: this.code, - }; - } + edit?: WorkspaceEdit; + + dianostics?: Diagnostic[]; + + kind?: CodeActionKind; + + constructor(title: string, kind?: CodeActionKind) { + this.title = title; + this.kind = kind; } +} - export class Hover { +export class CodeActionKind { + private static readonly sep = '.'; - public contents: vscode.MarkdownString[] | vscode.MarkedString[]; - public range: Range; + public static readonly Empty = new CodeActionKind(''); - 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 = contents; - } else if (vscMockHtmlContent.isMarkdownString(contents)) { - this.contents = [contents]; - } else { - this.contents = [contents]; - } - this.range = range; - } + 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); } - export enum DocumentHighlightKind { - Text = 0, - Read = 1, - Write = 2 + public contains(other: CodeActionKind): boolean { + return this.value === other.value || vscMockStrings.startsWith(other.value, this.value + CodeActionKind.sep); } +} - export class DocumentHighlight { +export class CodeLens { + range: Range; - range: Range; - kind: DocumentHighlightKind; + command: vscode.Command | undefined; - constructor(range: Range, kind: DocumentHighlightKind = DocumentHighlightKind.Text) { - this.range = range; - this.kind = kind; - } + constructor(range: Range, command?: vscode.Command) { + this.range = range; + this.command = command; + } - toJSON(): any { - return { - range: this.range, - kind: DocumentHighlightKind[this.kind] - }; - } + get isResolved(): boolean { + return !!this.command; } +} - 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; - 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; - this.containerName = containerName; - - if (typeof rangeOrContainer === 'string') { - this.containerName = rangeOrContainer; - } +export class MarkdownString { + value: string; - if (locationOrUri instanceof Location) { - this.location = locationOrUri; - } else if (rangeOrContainer instanceof Range) { - this.location = new Location(locationOrUri, rangeOrContainer); - } - } + isTrusted?: boolean; - toJSON(): any { - return { - name: this.name, - kind: SymbolKind[this.kind], - location: this.location, - containerName: this.containerName - }; - } + constructor(value?: string) { + this.value = value || ''; } - export class SymbolInformation2 extends SymbolInformation { - definingRange: Range; - children: SymbolInformation2[]; - constructor(name: string, kind: SymbolKind, containerName: string, location: Location) { - super(name, kind, containerName, location); + appendText(value: string): MarkdownString { + // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + this.value += value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); + return this; + } - this.children = []; - this.definingRange = location.range; - } + 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; - export enum CodeActionTrigger { - Automatic = 1, - Manual = 2, + documentation?: string | MarkdownString; + + constructor(label: string, documentation?: string | MarkdownString) { + this.label = label; + this.documentation = documentation; } +} - export class CodeAction { - title: string; +export class SignatureInformation { + label: string; - command?: vscode.Command; + documentation?: string | MarkdownString; - edit?: WorkspaceEdit; + parameters: ParameterInformation[]; - dianostics?: Diagnostic[]; + constructor(label: string, documentation?: string | MarkdownString) { + this.label = label; + this.documentation = documentation; + this.parameters = []; + } +} - kind?: CodeActionKind; +export class SignatureHelp { + signatures: SignatureInformation[]; - constructor(title: string, kind?: CodeActionKind) { - this.title = title; - this.kind = kind; - } + activeSignature: number; + + activeParameter: number; + + 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 class CodeActionKind { - private static readonly sep = '.'; +export enum CompletionItemTag { + Deprecated = 1, +} - 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'); +export interface CompletionItemLabel { + name: string; + signature?: string; + qualifier?: string; + type?: string; +} - constructor( - public readonly value: string - ) { } +export class CompletionItem { + label: string; - public append(parts: string): CodeActionKind { - return new CodeActionKind(this.value ? this.value + CodeActionKind.sep + parts : parts); - } + label2?: CompletionItemLabel; - public contains(other: CodeActionKind): boolean { - return this.value === other.value || vscMockStrings.startsWith(other.value, this.value + CodeActionKind.sep); - } + 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(): { + label: string; + label2?: CompletionItemLabel; + kind?: CompletionItemKind; + detail?: string; + documentation?: string | MarkdownString; + sortText?: string; + filterText?: string; + preselect?: boolean; + insertText?: string | SnippetString; + textEdit?: 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 CodeLens { +export class CompletionList { + isIncomplete?: boolean; - range: Range; + items: vscode.CompletionItem[]; - command: vscode.Command; + constructor(items: vscode.CompletionItem[] = [], isIncomplete = false) { + this.items = items; + this.isIncomplete = isIncomplete; + } +} - constructor(range: Range, command?: vscode.Command) { - this.range = range; - this.command = command; - } +export class CallHierarchyItem { + name: string; + + kind: SymbolKind; + + tags?: ReadonlyArray; + + detail?: string; + + uri: vscode.Uri; + + range: vscode.Range; + + selectionRange: vscode.Range; + + 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, +} - get isResolved(): boolean { - return !!this.command; +/** + * These values match very carefully the values of `TrackedRangeStickiness` + */ +export enum DecorationRangeBehavior { + /** + * TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges + */ + OpenOpen = 0, + /** + * TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + */ + ClosedClosed = 1, + /** + * TrackedRangeStickiness.GrowsOnlyWhenTypingBefore + */ + OpenClosed = 2, + /** + * TrackedRangeStickiness.GrowsOnlyWhenTypingAfter + */ + ClosedOpen = 3, +} + +// 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 MarkdownString { +export class DocumentLink { + range: Range; - value: string; - isTrusted?: boolean; + target: vscUri.URI; - constructor(value?: string) { - this.value = value || ''; + 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; + } +} - appendText(value: string): MarkdownString { - // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash - this.value += value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); - return this; +export class Color { + readonly red: number; + + readonly green: number; + + readonly blue: number; + + readonly alpha: number; + + constructor(red: number, green: number, blue: number, alpha: number) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + } +} + +export type IColorFormat = string | { opaque: string; transparent: string }; + +export class ColorInformation { + range: Range; + + color: Color; + + constructor(range: Range, color: Color) { + if (color && !(color instanceof Color)) { + throw illegalArgument('color'); + } + if (!Range.isRange(range) || range.isEmpty) { + throw illegalArgument('range'); } + this.range = range; + this.color = color; + } +} - appendMarkdown(value: string): MarkdownString { - this.value += value; - return this; +export class ColorPresentation { + label: string; + + textEdit?: TextEdit; + + additionalTextEdits?: TextEdit[]; + + constructor(label: string) { + if (!label || typeof label !== 'string') { + throw illegalArgument('label'); } + this.label = label; + } +} + +export enum ColorFormat { + RGB = 0, + HEX = 1, + HSL = 2, +} + +export enum SourceControlInputBoxValidationType { + Error = 0, + Warning = 1, + Information = 2, +} + +export enum TaskRevealKind { + Always = 1, + + Silent = 2, - appendCodeblock(code: string, language: string = ''): MarkdownString { - this.value += '\n```'; - this.value += language; - this.value += '\n'; - this.value += code; - this.value += '\n```\n'; - return this; - } - } + Never = 3, +} - export class ParameterInformation { +export enum TaskPanelKind { + Shared = 1, - label: string; - documentation?: string | MarkdownString; + Dedicated = 2, - constructor(label: string, documentation?: string | MarkdownString) { - this.label = label; - this.documentation = documentation; - } - } + New = 3, +} - export class SignatureInformation { +export class TaskGroup implements vscode.TaskGroup { + private _id: string; - label: string; - documentation?: string | MarkdownString; - parameters: ParameterInformation[]; + public isDefault = undefined; - constructor(label: string, documentation?: string | MarkdownString) { - this.label = label; - this.documentation = documentation; - this.parameters = []; - } - } + public static Clean: TaskGroup = new TaskGroup('clean', 'Clean'); + + public static Build: TaskGroup = new TaskGroup('build', 'Build'); - export class SignatureHelp { + public static Rebuild: TaskGroup = new TaskGroup('rebuild', 'Rebuild'); - signatures: SignatureInformation[]; - activeSignature: number; - activeParameter: number; + public static Test: TaskGroup = new TaskGroup('test', 'Test'); - constructor() { - this.signatures = []; + 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; } } - export enum CompletionTriggerKind { - Invoke = 0, - TriggerCharacter = 1, - TriggerForIncompleteCompletions = 2 + constructor(id: string, _label: string) { + if (typeof id !== 'string') { + throw illegalArgument('name'); + } + if (typeof _label !== 'string') { + throw illegalArgument('name'); + } + this._id = id; } - export interface CompletionContext { - triggerKind: CompletionTriggerKind; - triggerCharacter: string; + get id(): string { + return this._id; } +} - 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 - } +export class ProcessExecution implements vscode.ProcessExecution { + private _process: string; - export class CompletionItem { + private _args: string[] | undefined; - label: string; - kind: CompletionItemKind; - detail: string; - documentation: string | MarkdownString; - sortText: string; - filterText: string; - insertText: string | SnippetString; - range: Range; - textEdit: TextEdit; - additionalTextEdits: TextEdit[]; - command: vscode.Command; - - constructor(label: string, kind?: CompletionItemKind) { - this.label = label; - this.kind = kind; - } - - toJSON(): any { - return { - label: this.label, - kind: CompletionItemKind[this.kind], - detail: this.detail, - documentation: this.documentation, - sortText: this.sortText, - filterText: this.filterText, - insertText: this.insertText, - textEdit: this.textEdit - }; - } - } + private _options: vscode.ProcessExecutionOptions | undefined; - export class CompletionList { + constructor(process: string, options?: vscode.ProcessExecutionOptions); - isIncomplete?: boolean; + constructor(process: string, args: string[], options?: vscode.ProcessExecutionOptions); - items: vscode.CompletionItem[]; + 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; + } + } - constructor(items: vscode.CompletionItem[] = [], isIncomplete: boolean = false) { - this.items = items; - this.isIncomplete = isIncomplete; + if (this._args === undefined) { + this._args = []; } } - 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 + get process(): string { + return this._process; } - export enum StatusBarAlignment { - Left = 1, - Right = 2 + set process(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('process'); + } + this._process = value; } - export enum TextEditorLineNumbersStyle { - Off = 0, - On = 1, - Relative = 2 + get args(): string[] { + return this._args || []; } - export enum TextDocumentSaveReason { - Manual = 1, - AfterDelay = 2, - FocusOut = 3 + set args(value: string[]) { + if (!Array.isArray(value)) { + value = []; + } + this._args = value; } - export enum TextEditorRevealType { - Default = 0, - InCenter = 1, - InCenterIfOutsideViewport = 2, - AtTop = 3 + get options(): vscode.ProcessExecutionOptions { + return this._options || {}; } - export enum TextEditorSelectionChangeKind { - Keyboard = 1, - Mouse = 2, - Command = 3 + set options(value: vscode.ProcessExecutionOptions) { + this._options = value; } - /** - * These values match very carefully the values of `TrackedRangeStickiness` - */ - export enum DecorationRangeBehavior { - /** - * TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges - */ - OpenOpen = 0, - /** - * TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges - */ - ClosedClosed = 1, - /** - * TrackedRangeStickiness.GrowsOnlyWhenTypingBefore - */ - OpenClosed = 2, - /** - * TrackedRangeStickiness.GrowsOnlyWhenTypingAfter - */ - ClosedOpen = 3 - } - - export namespace TextEditorSelectionChangeKind { - export function fromValue(s: string) { - switch (s) { - case 'keyboard': return TextEditorSelectionChangeKind.Keyboard; - case 'mouse': return TextEditorSelectionChangeKind.Mouse; - case 'api': return TextEditorSelectionChangeKind.Command; - } - return undefined; - } + // 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'); } +} + +export class ShellExecution implements vscode.ShellExecution { + private _commandLine = ''; + + private _command: string | vscode.ShellQuotedString = ''; + + private _args: (string | vscode.ShellQuotedString)[] = []; - export class DocumentLink { + private _options: vscode.ShellExecutionOptions | undefined; - range: Range; + constructor(commandLine: string, options?: vscode.ShellExecutionOptions); - target: vscUri.URI; + constructor( + command: string | vscode.ShellQuotedString, + args: (string | vscode.ShellQuotedString)[], + options?: vscode.ShellExecutionOptions, + ); - constructor(range: Range, target: vscUri.URI) { - if (target && !(target instanceof vscUri.URI)) { - throw illegalArgument('target'); + 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 (!Range.isRange(range) || range.isEmpty) { - throw illegalArgument('range'); + if (typeof arg0 !== 'string' && typeof arg0.value !== 'string') { + throw illegalArgument('command'); + } + this._command = arg0; + this._args = arg1 as (string | vscode.ShellQuotedString)[]; + this._options = arg2; + } else { + if (typeof arg0 !== 'string') { + throw illegalArgument('commandLine'); } - this.range = range; - this.target = target; + this._commandLine = arg0; + this._options = arg1; } } - export class Color { - readonly red: number; - readonly green: number; - readonly blue: number; - readonly alpha: number; + get commandLine(): string { + return this._commandLine; + } - constructor(red: number, green: number, blue: number, alpha: number) { - this.red = red; - this.green = green; - this.blue = blue; - this.alpha = alpha; + set commandLine(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('commandLine'); } + this._commandLine = value; } - export type IColorFormat = string | { opaque: string, transparent: string }; - - export class ColorInformation { - range: Range; - - color: Color; + get command(): string | vscode.ShellQuotedString { + return this._command; + } - 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; + set command(value: string | vscode.ShellQuotedString) { + if (typeof value !== 'string' && typeof value.value !== 'string') { + throw illegalArgument('command'); } + this._command = value; } - export class ColorPresentation { - label: string; - textEdit?: TextEdit; - additionalTextEdits?: TextEdit[]; + get args(): (string | vscode.ShellQuotedString)[] { + return this._args; + } - constructor(label: string) { - if (!label || typeof label !== 'string') { - throw illegalArgument('label'); - } - this.label = label; - } + set args(value: (string | vscode.ShellQuotedString)[]) { + this._args = value || []; } - export enum ColorFormat { - RGB = 0, - HEX = 1, - HSL = 2 + get options(): vscode.ShellExecutionOptions { + return this._options || {}; } - export enum SourceControlInputBoxValidationType { - Error = 0, - Warning = 1, - Information = 2 + set options(value: vscode.ShellExecutionOptions) { + this._options = value; } - export enum TaskRevealKind { - Always = 1, + // 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'); + } +} - Silent = 2, +export enum ShellQuoting { + Escape = 1, + Strong = 2, + Weak = 3, +} - Never = 3 - } +export enum TaskScope { + Global = 1, + Workspace = 2, +} - export enum TaskPanelKind { - Shared = 1, +export class Task implements vscode.Task { + private static ProcessType = 'process'; - Dedicated = 2, + private static ShellType = 'shell'; - New = 3 - } + private static EmptyType = '$empty'; - export class TaskGroup implements vscode.TaskGroup { + private __id: string | undefined; - private _id: string; + private _definition!: vscode.TaskDefinition; - public static Clean: TaskGroup = new TaskGroup('clean', 'Clean'); + private _scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder | undefined; - public static Build: TaskGroup = new TaskGroup('build', 'Build'); + private _name!: string; - public static Rebuild: TaskGroup = new TaskGroup('rebuild', 'Rebuild'); + private _execution: ProcessExecution | ShellExecution | undefined; - public static Test: TaskGroup = new TaskGroup('test', 'Test'); + private _problemMatchers: string[]; - 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; - } - } + private _hasDefinedMatchers: boolean; - constructor(id: string, _label: string) { - if (typeof id !== 'string') { - throw illegalArgument('name'); - } - if (typeof _label !== 'string') { - throw illegalArgument('name'); - } - this._id = id; - } + private _isBackground: boolean; - get id(): string { - return this._id; - } - } + private _source!: string; - export class ProcessExecution implements vscode.ProcessExecution { + private _group: TaskGroup | undefined; - private _process: string; - private _args: string[]; - private _options: vscode.ProcessExecutionOptions; + private _presentationOptions: vscode.TaskPresentationOptions; - 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; - this._options = varg2; - } else { - this._options = varg1; - } - } - if (this._args === void 0) { - this._args = []; - } - } + private _runOptions: vscode.RunOptions; + constructor( + definition: vscode.TaskDefinition, + name: string, + source: string, + execution?: ProcessExecution | ShellExecution, + problemMatchers?: string | string[], + ); - get process(): string { - return this._process; - } + constructor( + definition: vscode.TaskDefinition, + scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder, + name: string, + source: string, + execution?: ProcessExecution | ShellExecution, + problemMatchers?: string | string[], + ); - set process(value: string) { - if (typeof value !== 'string') { - throw illegalArgument('process'); - } - this._process = value; + 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[]; } - - get args(): string[] { - return this._args; + 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); + } - set args(value: string[]) { - if (!Array.isArray(value)) { - value = []; - } - this._args = value; - } + get _id(): string | undefined { + return this.__id; + } - get options(): vscode.ProcessExecutionOptions { - return this._options; - } + set _id(value: string | undefined) { + this.__id = value; + } - set options(value: vscode.ProcessExecutionOptions) { - this._options = value; + private clear(): void { + if (this.__id === undefined) { + return; } + this.__id = undefined; + this._scope = undefined; + this.computeDefinitionBasedOnExecution(); + } - 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'); + 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(), + }; } } - export class ShellExecution implements vscode.ShellExecution { - - private _commandLine: string; - private _command: string | vscode.ShellQuotedString; - private _args: (string | vscode.ShellQuotedString)[]; - private _options: vscode.ShellExecutionOptions; + get definition(): vscode.TaskDefinition { + return this._definition; + } - 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)[]; - this._options = arg2; - } else { - if (typeof arg0 !== 'string') { - throw illegalArgument('commandLine'); - } - this._commandLine = arg0; - this._options = arg1; - } + set definition(value: vscode.TaskDefinition) { + if (value === undefined || value === null) { + throw illegalArgument("Kind can't be undefined or null"); } + this.clear(); + this._definition = value; + } - get commandLine(): string { - return this._commandLine; - } + get scope(): vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder | undefined { + return this._scope; + } - set commandLine(value: string) { - if (typeof value !== 'string') { - throw illegalArgument('commandLine'); - } - this._commandLine = value; - } + set target(value: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder) { + this.clear(); + this._scope = value; + } - get command(): string | vscode.ShellQuotedString { - return this._command; - } + get name(): string { + return this._name; + } - set command(value: string | vscode.ShellQuotedString) { - if (typeof value !== 'string' && typeof value.value !== 'string') { - throw illegalArgument('command'); - } - this._command = value; + set name(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('name'); } + this.clear(); + this._name = value; + } - get args(): (string | vscode.ShellQuotedString)[] { - return this._args; - } + get execution(): ProcessExecution | ShellExecution | undefined { + return this._execution; + } - set args(value: (string | vscode.ShellQuotedString)[]) { - this._args = value || []; + set execution(value: ProcessExecution | ShellExecution | undefined) { + if (value === null) { + value = undefined; } - - get options(): vscode.ShellExecutionOptions { - return this._options; + this.clear(); + this._execution = value; + const { type } = this._definition; + if (Task.EmptyType === type || Task.ProcessType === type || Task.ShellType === type) { + this.computeDefinitionBasedOnExecution(); } + } - set options(value: vscode.ShellExecutionOptions) { - this._options = value; - } + get problemMatchers(): string[] { + return this._problemMatchers; + } - 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'); + set problemMatchers(value: string[]) { + if (!Array.isArray(value)) { + this.clear(); + this._problemMatchers = []; + this._hasDefinedMatchers = false; + } else { + this.clear(); + this._problemMatchers = value; + this._hasDefinedMatchers = true; } } - export enum ShellQuoting { - Escape = 1, - Strong = 2, - Weak = 3 + get hasDefinedMatchers(): boolean { + return this._hasDefinedMatchers; } - export enum TaskScope { - Global = 1, - Workspace = 2 + get isBackground(): boolean { + return this._isBackground; } - export class Task implements vscode.Task { - - private __id: string; + set isBackground(value: boolean) { + if (value !== true && value !== false) { + value = false; + } + this.clear(); + this._isBackground = value; + } - private _definition: vscode.TaskDefinition; - private _scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder; - private _name: string; - private _execution: ProcessExecution | ShellExecution; - private _problemMatchers: string[]; - private _hasDefinedMatchers: boolean; - private _isBackground: boolean; - private _source: string; - private _group: TaskGroup; - private _presentationOptions: vscode.TaskPresentationOptions; + get source(): string { + return this._source; + } - 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; + set source(value: string) { + if (typeof value !== 'string' || value.length === 0) { + throw illegalArgument('source must be a string of length > 0'); } + this.clear(); + this._source = value; + } - get _id(): string { - return this.__id; - } + get group(): TaskGroup | undefined { + return this._group; + } - set _id(value: string) { - this.__id = value; + set group(value: TaskGroup | undefined) { + if (value === null) { + value = undefined; } + this.clear(); + this._group = value; + } - private clear(): void { - if (this.__id === void 0) { - return; - } - this.__id = undefined; - this._scope = undefined; - this._definition = undefined; - if (this._execution instanceof ProcessExecution) { - this._definition = { - type: 'process', - id: this._execution.computeId() - }; - } else if (this._execution instanceof ShellExecution) { - this._definition = { - type: 'shell', - id: this._execution.computeId() - }; - } - } + get presentationOptions(): vscode.TaskPresentationOptions { + return this._presentationOptions; + } - get definition(): vscode.TaskDefinition { - return this._definition; + set presentationOptions(value: vscode.TaskPresentationOptions) { + if (value === null || value === undefined) { + value = Object.create(null); } + this.clear(); + this._presentationOptions = value; + } - set definition(value: vscode.TaskDefinition) { - if (value === void 0 || value === null) { - throw illegalArgument('Kind can\'t be undefined or null'); - } - this.clear(); - this._definition = value; - } + get runOptions(): vscode.RunOptions { + return this._runOptions; + } - get scope(): vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder { - return this._scope; + set runOptions(value: vscode.RunOptions) { + if (value === null || value === undefined) { + value = Object.create(null); } + this.clear(); + this._runOptions = value; + } +} - set target(value: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder) { - this.clear(); - this._scope = value; - } +export enum ProgressLocation { + SourceControl = 1, + Window = 10, + Notification = 15, +} - get name(): string { - return this._name; - } +export enum TreeItemCollapsibleState { + None = 0, + Collapsed = 1, + Expanded = 2, +} - set name(value: string) { - if (typeof value !== 'string') { - throw illegalArgument('name'); - } - this.clear(); - this._name = value; - } +/** + * 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; - get execution(): ProcessExecution | ShellExecution { - return this._execution; - } +export class TreeItem { + label?: string | vscode.TreeItemLabel; + id?: string; - set execution(value: ProcessExecution | ShellExecution) { - if (value === null) { - value = undefined; - } - this.clear(); - this._execution = value; - } + resourceUri?: vscUri.URI; - get problemMatchers(): string[] { - return this._problemMatchers; - } + iconPath?: string | IconPath; - set problemMatchers(value: string[]) { - if (!Array.isArray(value)) { - this._problemMatchers = []; - this._hasDefinedMatchers = false; - return; - } - this.clear(); - this._problemMatchers = value; - this._hasDefinedMatchers = true; - } + command?: vscode.Command; - get hasDefinedMatchers(): boolean { - return this._hasDefinedMatchers; - } + contextValue?: string; - get isBackground(): boolean { - return this._isBackground; - } + tooltip?: string; - set isBackground(value: boolean) { - if (value !== true && value !== false) { - value = false; - } - this.clear(); - this._isBackground = value; - } + constructor(label: string, collapsibleState?: vscode.TreeItemCollapsibleState); - get source(): string { - return this._source; - } + constructor(resourceUri: vscUri.URI, collapsibleState?: vscode.TreeItemCollapsibleState); - 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; + constructor( + arg1: string | vscUri.URI, + public collapsibleState: vscode.TreeItemCollapsibleState = TreeItemCollapsibleState.None, + ) { + if (arg1 instanceof vscUri.URI) { + this.resourceUri = arg1; + } else { + this.label = arg1; } + } +} - get group(): TaskGroup { - return this._group; - } +export class ThemeIcon { + static readonly File = new ThemeIcon('file'); - set group(value: TaskGroup) { - if (value === void 0 || value === null) { - this._group = undefined; - return; - } - this.clear(); - this._group = value; - } + static readonly Folder = new ThemeIcon('folder'); - get presentationOptions(): vscode.TaskPresentationOptions { - return this._presentationOptions; - } + readonly id: string; - set presentationOptions(value: vscode.TaskPresentationOptions) { - if (value === null) { - value = undefined; - } - this.clear(); - this._presentationOptions = value; - } + private constructor(id: string) { + this.id = id; } +} +export class ThemeColor { + id: string; - export enum ProgressLocation { - SourceControl = 1, - Window = 10, - Notification = 15 + constructor(id: string) { + this.id = id; } +} - export class TreeItem { +export enum ConfigurationTarget { + Global = 1, - label?: string; - resourceUri?: vscUri.URI; - iconPath?: string | vscUri.URI | { light: string | vscUri.URI; dark: string | vscUri.URI }; - command?: vscode.Command; - contextValue?: string; - tooltip?: string; + Workspace = 2, - 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; + 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'); } } + 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 enum TreeItemCollapsibleState { - None = 0, - Collapsed = 1, - Expanded = 2 + // eslint-disable-next-line class-methods-use-this + public pathToRelative(from: string, to: string): string { + return relative(from, to); } +} + +export class Breakpoint { + readonly enabled: boolean; + + readonly condition?: string; + + readonly hitCondition?: string; - export class ThemeIcon { - static readonly File = new ThemeIcon('file'); + readonly logMessage?: string; - static readonly Folder = new ThemeIcon('folder'); + 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; + } + } +} - readonly id: string; +export class SourceBreakpoint extends Breakpoint { + readonly location: Location; - private constructor(id: string) { - this.id = id; + 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 ThemeColor { - id: string; - constructor(id: string) { - this.id = id; +export class FunctionBreakpoint extends Breakpoint { + readonly functionName: string; + + constructor( + functionName: string, + enabled?: boolean, + condition?: string, + hitCondition?: string, + logMessage?: string, + ) { + super(enabled, condition, hitCondition, logMessage); + if (!functionName) { + throw illegalArgument('functionName'); } + this.functionName = functionName; } +} - export enum ConfigurationTarget { - Global = 1, +export class DebugAdapterExecutable { + readonly command: string; - Workspace = 2, + readonly args: string[]; - WorkspaceFolder = 3 + constructor(command: string, args?: string[]) { + this.command = command; + this.args = args || []; } +} - export class RelativePattern implements IRelativePattern { - base: string; - pattern: string; +export class DebugAdapterServer { + readonly port: number; - constructor(base: vscode.WorkspaceFolder | string, pattern: string) { - if (typeof base !== 'string') { - if (!base || !vscUri.URI.isUri(base.uri)) { - throw illegalArgument('base'); - } - } + readonly host?: string; - if (typeof pattern !== 'string') { - throw illegalArgument('pattern'); - } + constructor(port: number, host?: string) { + this.port = port; + this.host = host; + } +} - this.base = typeof base === 'string' ? base : base.uri.fsPath; - this.pattern = pattern; - } +export enum LogLevel { + Trace = 1, + Debug = 2, + Info = 3, + Warning = 4, + Error = 5, + Critical = 6, + Off = 7, +} - public pathToRelative(from: string, to: string): string { - return relative(from, to); - } - } +// #region file api - export class Breakpoint { +export enum FileChangeType { + Changed = 1, + Created = 2, + Deleted = 3, +} - readonly enabled: boolean; - readonly condition?: string; - readonly hitCondition?: string; - readonly logMessage?: string; +export class FileSystemError extends Error { + static FileExists(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryExists', FileSystemError.FileExists); + } - 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 FileNotFound(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryNotFound', FileSystemError.FileNotFound); } - export class SourceBreakpoint extends Breakpoint { - readonly location: Location; + static FileNotADirectory(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryNotADirectory', FileSystemError.FileNotADirectory); + } - 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; - } + static FileIsADirectory(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryIsADirectory', FileSystemError.FileIsADirectory); } - export class FunctionBreakpoint extends Breakpoint { - readonly functionName: string; + static NoPermissions(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'NoPermissions', FileSystemError.NoPermissions); + } - constructor(functionName: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { - super(enabled, condition, hitCondition, logMessage); - if (!functionName) { - throw illegalArgument('functionName'); - } - this.functionName = functionName; - } + static Unavailable(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'Unavailable', FileSystemError.Unavailable); } - export class DebugAdapterExecutable { - readonly command: string; - readonly args: string[]; + constructor(uriOrMessage?: string | vscUri.URI, code?: string, terminator?: () => void) { + super(vscUri.URI.isUri(uriOrMessage) ? uriOrMessage.toString(true) : uriOrMessage); + this.name = code ? `${code} (FileSystemError)` : `FileSystemError`; - constructor(command: string, args?: string[]) { - this.command = command; - this.args = args; + Object.setPrototypeOf(this, FileSystemError.prototype); + + if (typeof Error.captureStackTrace === 'function' && typeof terminator === 'function') { + // nice stack traces + Error.captureStackTrace(this, terminator); } } - export enum LogLevel { - Trace = 1, - Debug = 2, - Info = 3, - Warning = 4, - Error = 5, - Critical = 6, - Off = 7 + // eslint-disable-next-line class-methods-use-this + public get code(): string { + return ''; } +} - //#region file api +// #endregion - export enum FileChangeType { - Changed = 1, - Created = 2, - Deleted = 3, +// #region folding api + +export class FoldingRange { + start: number; + + end: number; + + kind?: FoldingRangeKind; + + constructor(start: number, end: number, kind?: FoldingRangeKind) { + this.start = start; + this.end = end; + this.kind = kind; } +} - export class FileSystemError extends Error { +export enum FoldingRangeKind { + Comment = 1, + Imports = 2, + Region = 3, +} - 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); - } +// #endregion + +export enum CommentThreadCollapsibleState { + /** + * Determines an item is collapsed + */ + Collapsed = 0, + /** + * Determines an item is expanded + */ + Expanded = 1, +} - constructor(uriOrMessage?: string | vscUri.URI, code?: string, terminator?: Function) { - super(vscUri.URI.isUri(uriOrMessage) ? uriOrMessage.toString(true) : uriOrMessage); - this.name = code ? `${code} (FileSystemError)` : `FileSystemError`; +export class QuickInputButtons { + static readonly Back: vscode.QuickInputButton = { iconPath: vscUri.URI.file('back') }; +} - // 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 (Object).setPrototypeOf === 'function') { - (Object).setPrototypeOf(this, FileSystemError.prototype); - } +export enum SymbolTag { + Deprecated = 1, +} - if (typeof Error.captureStackTrace === 'function' && typeof terminator === 'function') { - // nice stack traces - Error.captureStackTrace(this, terminator); - } - } - } +export class TypeHierarchyItem { + name: string; - //#endregion + kind: SymbolKind; - //#region folding api + tags?: ReadonlyArray; - export class FoldingRange { + detail?: string; - start: number; + uri: vscode.Uri; - end: number; + range: Range; - kind?: FoldingRangeKind; + selectionRange: Range; - constructor(start: number, end: number, kind?: FoldingRangeKind) { - this.start = start; - this.end = end; - this.kind = kind; - } + 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; } +} + +export declare type LSPObject = { + [key: string]: LSPAny; +}; + +export declare type LSPArray = LSPAny[]; - export enum FoldingRangeKind { - Comment = 1, - Imports = 2, - Region = 3 +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; } +} - //#endregion +export class CancellationError extends Error {} +export class LSPCancellationError extends CancellationError { + data; - export enum CommentThreadCollapsibleState { - /** - * Determines an item is collapsed - */ - Collapsed = 0, - /** - * Determines an item is expanded - */ - Expanded = 1 + constructor(data: any) { + super(); + this.data = data; } } diff --git a/src/test/mocks/vsc/htmlContent.ts b/src/test/mocks/vsc/htmlContent.ts index 5be4dd47fc92..df07c6ac3b1c 100644 --- a/src/test/mocks/vsc/htmlContent.ts +++ b/src/test/mocks/vsc/htmlContent.ts @@ -1,97 +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 { +export class MarkdownString implements IMarkdownString { + value: string; - value: string; - isTrusted?: boolean; + isTrusted?: boolean; - constructor(value: string = '') { - this.value = value; - } + 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; - } + 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 (thing).value === 'string' - && (typeof (thing).isTrusted === 'boolean' || (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 (thing).value === 'string' && + (typeof (thing).isTrusted === 'boolean' || + (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 d9f6aded83f0..152beb64cdf4 100644 --- a/src/test/mocks/vsc/index.ts +++ b/src/test/mocks/vsc/index.ts @@ -1,158 +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 * as vscMockExtHostedTypes from './extHostedTypes'; +export * as vscUri from './uri'; -export namespace vscMock { - // This is one of the very few classes that we need in our unit tests. - // It is constructed in a number of places, and this is required for verification. - // Using mocked objects for verfications does not work in typemoq. - export class Uri implements vscode.Uri { - private constructor(public readonly scheme: string, public readonly authority: string, - public readonly path: string, public readonly query: string, - public readonly fragment: string, public readonly fsPath) { +const escapeCodiconsRegex = /(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi; +export function escapeCodicons(text: string): string { + return text.replace(escapeCodiconsRegex, (match, escaped) => (escaped ? match : `\\${match}`)); +} - } - public static file(path: string): Uri { - return new Uri('file', '', path, '', '', path); - } - public static parse(value: string): Uri { - return new Uri('http', '', value, '', '', value); - } - public with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): vscode.Uri { - throw new Error('Not implemented'); - } - public toString(skipEncoding?: boolean): string { - return this.fsPath; - } - public toJSON(): any { - return this.fsPath; - } - } +export class ThemeIcon { + static readonly File: ThemeIcon; - export class Disposable { - constructor(private callOnDispose: Function) { - } - public dispose(): any { - if (this.callOnDispose) { - this.callOnDispose(); + 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 implements vscode.EventEmitter { + private _callOnDispose: (() => void) | undefined; - public event: vscode.Event; - public emitter: NodeEventEmitter; - constructor() { - 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(); - } + constructor(callOnDispose: () => void) { + this._callOnDispose = callOnDispose; + } - protected add = (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable => { - this.emitter.addListener('evt', listener); - return { - dispose: () => { - this.emitter.removeListener('evt', listener); - } - } as any as Disposable; + dispose(): void { + if (typeof this._callOnDispose === 'function') { + this._callOnDispose(); + this._callOnDispose = undefined; } } +} + +// 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 | Record; + comment: string | string[]; + }): string; - export class CancellationToken extends EventEmitter implements vscode.CancellationToken { - public isCancellationRequested!: boolean; - public onCancellationRequested: vscode.Event; - constructor() { - super(); - this.onCancellationRequested = this.add.bind(this); + export function t( + message: + | string + | { + message: string; + args?: Array | Record; + comment: string | string[]; + }, + ...args: unknown[] + ): string { + let _message = message; + let _args: unknown[] | Record | undefined = args; + if (typeof message !== 'string') { + _message = message.message; + _args = message.args ?? args; } - public cancel() { - this.isCancellationRequested = true; - this.fire(); + + if ((_args as Array).length > 0) { + return (_message as string).replace(/{(\d+)}/g, (match, number) => + (_args as Array)[number] === undefined ? match : (_args as Array)[number], + ); } + return _message as string; + } + export const bundle: { [key: string]: string } | undefined = undefined; + export const uri: vscode.Uri | undefined = undefined; +} + +export class EventEmitter implements vscode.EventEmitter { + public event: vscode.Event; + + public emitter: NodeEventEmitter; + + constructor() { + this.event = (this.add.bind(this) as unknown) as vscode.Event; + this.emitter = new NodeEventEmitter(); } - export class CancellationTokenSource { - public token: CancellationToken; - constructor() { - this.token = new CancellationToken(); + public fire(data?: T): void { + this.emitter.emit('evt', data); + } + + public dispose(): void { + this.emitter.removeAllListeners(); + } + + protected add = ( + listener: (e: T) => void, + _thisArgs?: EventEmitter, + _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 extends EventEmitter implements vscode.CancellationToken { + public isCancellationRequested!: boolean; + + public onCancellationRequested: vscode.Event; + + constructor() { + super(); + this.onCancellationRequested = this.add.bind(this) as vscode.Event; + } + + public cancel(): void { + this.isCancellationRequested = true; + this.fire(); + } +} + +export class CancellationTokenSource { + public token: CancellationToken; + + constructor() { + this.token = new CancellationToken(); + } + + public cancel(): void { + this.token.cancel(); + } + + public dispose(): void { + this.token.dispose(); + } +} + +export class CodeAction { + public title: string; + + public edit?: vscode.WorkspaceEdit; + + public diagnostics?: vscode.Diagnostic[]; + + public command?: vscode.Command; + + public kind?: CodeActionKind; + + public isPreferred?: boolean; + + constructor(_title: string, _kind?: CodeActionKind) { + this.title = _title; + this.kind = _kind; + } +} + +export enum CompletionItemKind { + Text = 0, + Method = 1, + Function = 2, + Constructor = 3, + Field = 4, + Variable = 5, + Class = 6, + Interface = 7, + Module = 8, + Property = 9, + Unit = 10, + Value = 11, + Enum = 12, + Keyword = 13, + Snippet = 14, + Color = 15, + Reference = 17, + File = 16, + Folder = 18, + EnumMember = 19, + Constant = 20, + Struct = 21, + Event = 22, + Operator = 23, + TypeParameter = 24, + User = 25, + Issue = 26, +} +export enum SymbolKind { + File = 0, + Module = 1, + Namespace = 2, + Package = 3, + Class = 4, + Method = 5, + Property = 6, + Field = 7, + Constructor = 8, + Enum = 9, + Interface = 10, + Function = 11, + Variable = 12, + Constant = 13, + String = 14, + Number = 15, + Boolean = 16, + Array = 17, + Object = 18, + Key = 19, + Null = 20, + EnumMember = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, +} +export enum IndentAction { + None = 0, + Indent = 1, + IndentOutdent = 2, + Outdent = 3, +} + +export 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; + } + + public static isMarkdownString(thing?: string | MarkdownString | unknown): thing is vscode.MarkdownString { + if (thing instanceof MarkdownString) { + return true; } - public cancel(): void { - this.token.cancel(); + 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; + } + + 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 dispose(): void { - this.token.dispose(); + if (Array.isArray(contents)) { + this.contents = contents; + } else if (MarkdownString.isMarkdownString(contents)) { + this.contents = [contents]; + } else { + this.contents = [contents]; } + this.range = range; + } +} + +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 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}`); + } + + public intersects(other: CodeActionKind): boolean { + return this._value.includes(other._value) || other._value.includes(this._value); + } + + public contains(other: CodeActionKind): boolean { + return this._value.startsWith(other._value); + } + + public get value(): string { + return this._value; + } +} + +export interface DebugAdapterExecutableOptions { + env?: { [key: string]: string }; + cwd?: string; +} + +export class DebugAdapterServer { + constructor(public readonly port: number, public readonly host?: string) {} +} +export class DebugAdapterExecutable { + constructor( + public readonly command: string, + public readonly args: string[] = [], + public readonly options?: DebugAdapterExecutableOptions, + ) {} +} + +export enum FileType { + Unknown = 0, + File = 1, + Directory = 2, + SymbolicLink = 64, +} + +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; } - 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 - } - 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 + /** + * 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 f901c5f7d9ce..b05107e0be79 100644 --- a/src/test/mocks/vsc/position.ts +++ b/src/test/mocks/vsc/position.ts @@ -1,157 +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 16d40ff0bb12..538e9ec7b9d2 100644 --- a/src/test/mocks/vsc/range.ts +++ b/src/test/mocks/vsc/range.ts @@ -1,384 +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) { - return null; - } - if (resultStartLineNumber === resultEndLineNumber && resultStartColumn > resultEndColumn) { - 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) { - 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 5c750fed8847..84b165f03b4c 100644 --- a/src/test/mocks/vsc/selection.ts +++ b/src/test/mocks/vsc/selection.ts @@ -1,211 +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; } + return true; + } - /** - * Test if `obj` is an `ISelection`. - */ - public static isISelection(obj: any): obj is ISelection { - return ( - obj - && (typeof obj.selectionStartLineNumber === 'number') - && (typeof obj.selectionStartColumn === 'number') - && (typeof obj.positionLineNumber === 'number') - && (typeof obj.positionColumn === 'number') - ); - } - - /** - * Create with a direction. - */ - public static createWithDirection(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, direction: SelectionDirection): Selection { - - if (direction === SelectionDirection.LTR) { - return new Selection(startLineNumber, startColumn, endLineNumber, endColumn); - } + /** + * 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 9b5ef94178cf..5df8bcac5905 100644 --- a/src/test/mocks/vsc/telemetryReporter.ts +++ b/src/test/mocks/vsc/telemetryReporter.ts @@ -3,14 +3,9 @@ 'use strict'; -// tslint:disable:all -import * as telemetry from 'vscode-extension-telemetry'; -export class vscMockTelemetryReporter implements telemetry.default { - constructor() { - // - } - +export class vscMockTelemetryReporter { + // 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 e91531924823..671c60c1ba65 100644 --- a/src/test/mocks/vsc/uri.ts +++ b/src/test/mocks/vsc/uri.ts @@ -1,474 +1,731 @@ +/* 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'; -export namespace vscUri { - const platform = { - isWindows: /^win/.test(process.platform) - }; +import * as pathImport from 'path'; +import { CharCode } from './charCode'; - // tslint:disable:all +const isWindows = /^win/.test(process.platform); - function _encode(ch: string): string { - return '%' + ch.charCodeAt(0).toString(16).toUpperCase(); - } +const _schemePattern = /^\w[\w\d+.-]*$/; +const _singleSlashStart = /^\//; +const _doubleSlashStart = /^\/\//; - // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent - function encodeURIComponent2(str: string): string { - return encodeURIComponent(str).replace(/[!'()*]/g, _encode); - } +const _empty = ''; +const _slash = '/'; +const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; - function encodeNoop(str: string): string { - return str.replace(/[#?]/, _encode); - } +const _pathSepMarker = isWindows ? 1 : undefined; +let _throwOnMissingSchema = true; - const _schemePattern = /^\w[\w\d+.-]*$/; - const _singleSlashStart = /^\//; - const _doubleSlashStart = /^\/\//; +/** + * @internal + */ +export function setUriThrowOnMissingScheme(value: boolean): boolean { + const old = _throwOnMissingSchema; + _throwOnMissingSchema = value; + return old; +} - function _validateUri(ret: URI): void { - // 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.'); +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 ("//")', + ); } + } +} - // 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) { + 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; + 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; + } + if (!thing) { + return false; } + return ( + typeof (thing).authority === 'string' && + typeof (thing).fragment === 'string' && + typeof (thing).path === 'string' && + typeof (thing).query === 'string' && + typeof (thing).scheme === 'string' && + typeof (thing).fsPath === 'function' && + typeof (thing).with === 'function' && + typeof (thing).toString === 'function' + ); } - const _empty = ''; - const _slash = '/'; - const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; - const _driveLetterPath = /^\/[a-zA-Z]:/; - const _upperCaseDrive = /^(\/)?([A-Z]:)/; - const _driveLetter = /^[a-zA-Z]:/; + /** + * 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 paths - * (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. */ - export class URI implements UriComponents { + readonly authority: string; - static isUri(thing: any): thing is URI { - if (thing instanceof URI) { - return true; - } - if (!thing) { - return false; - } - return typeof (thing).authority === 'string' - && typeof (thing).fragment === 'string' - && typeof (thing).path === 'string' - && typeof (thing).query === 'string' - && typeof (thing).scheme === '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); - - /** - * @internal - */ - protected constructor(components: UriComponents); - - /** - * @internal - */ - protected constructor(schemeOrData: string | UriComponents, authority?: string, path?: string, query?: string, fragment?: string) { - - 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 = schemeOrData || _empty; - this.authority = authority || _empty; - this.path = path || _empty; - this.query = query || _empty; - this.fragment = fragment || _empty; - _validateUri(this); - } - } + /** + * path is the '/some/path' part of 'http://www.msft.com/some/path?query#fragment'. + */ + readonly path: string; - // ---- filesystem path ----------------------- + /** + * query is the 'query' part of 'http://www.msft.com/some/path?query#fragment'. + */ + readonly query: string; - /** - * Returns a string representing the corresponding file system path of this URI. - * Will handle UNC paths and normalize windows drive letters to lower-case. Also - * uses the platform specific path separator. Will *not* validate the path for - * invalid characters and semantics. Will *not* look at the scheme of this URI. - */ - get fsPath(): string { - return _makeFsPath(this); - } + /** + * fragment is the 'fragment' part of 'http://www.msft.com/some/path?query#fragment'. + */ + readonly fragment: string; - // ---- modify to new ------------------------- + /** + * @internal + */ + protected constructor( + scheme: string, + authority?: string, + path?: string, + query?: string, + fragment?: string, + _strict?: boolean, + ); - public with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): URI { + /** + * @internal + */ + protected constructor(components: UriComponents); - if (!change) { - return this; - } + /** + * @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; - let { scheme, authority, path, query, fragment } = change; - if (scheme === void 0) { - scheme = this.scheme; - } else if (scheme === null) { - scheme = _empty; - } - if (authority === void 0) { - authority = this.authority; - } else if (authority === null) { - authority = _empty; - } - if (path === void 0) { - path = this.path; - } else if (path === null) { - path = _empty; - } - if (query === void 0) { - query = this.query; - } else if (query === null) { - query = _empty; - } - if (fragment === void 0) { - fragment = this.fragment; - } else if (fragment === null) { - fragment = _empty; - } + _validateUri(this, _strict); + } + } - if (scheme === this.scheme - && authority === this.authority - && path === this.path - && query === this.query - && fragment === this.fragment) { + // ---- filesystem path ----------------------- - return this; - } + /** + * Returns a string representing the corresponding file system path of this URI. + * Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the + * platform specific path separator. + * + * * Will *not* validate the path for invalid characters and semantics. + * * Will *not* look at the scheme of this URI. + * * The result shall *not* be used for display purposes but for accessing a file on disk. + * + * + * The *difference* to `URI#path` is the use of the platform specific separator and the handling + * of UNC paths. See the below sample of a file-uri with an authority (UNC path). + * + * ```ts + const u = URI.parse('file://server/c$/folder/file.txt') + u.authority === 'server' + u.path === '/shares/c$/file.txt' + u.fsPath === '\\server\c$\folder\file.txt' + ``` + * + * Using `URI#path` to read a file (using fs-apis) would not be enough because parts of the path, + * namely the server name, would be missing. Therefore `URI#fsPath` exists - it's sugar to ease working + * with URIs that represent files on disk (`file` scheme). + */ + get fsPath(): string { + // if (this.scheme !== 'file') { + // console.warn(`[UriError] calling fsPath with scheme ${this.scheme}`); + // } + return _makeFsPath(this); + } - return new _URI(scheme, authority, path, query, fragment); + // ---- modify to new ------------------------- + + with(change: { + scheme?: string; + authority?: string | null; + path?: string | null; + query?: string | null; + fragment?: string | null; + }): URI { + if (!change) { + return this; } - // ---- parse & validate ------------------------ - - public static parse(value: string): 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), - ); + 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; } - public static file(path: string): URI { + if ( + scheme === this.scheme && + authority === this.authority && + path === this.path && + query === this.query && + fragment === this.fragment + ) { + return this; + } - let authority = _empty; + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return new _URI(scheme, authority, path, query, fragment); + } - // normalize to fwd-slashes on windows, - // on other systems bwd-slashes are valid - // filename character, eg /f\oo/ba\r.txt - if (platform.isWindows) { - path = path.replace(/\\/g, _slash); - } + // ---- parse & validate ------------------------ - // check for authority as used in UNC shares - // or use the path as given - if (path[0] === _slash && path[1] === _slash) { - let 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; - } - } + /** + * 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, + ); + } - // Ensure that path starts with a slash - // or that it is at least a slash - if (_driveLetter.test(path)) { - path = _slash + path; + /** + * Creates a new URI from a file system path, e.g. `c:\my\files`, + * `/usr/home`, or `\\server\share\some\path`. + * + * The *difference* between `URI#parse` and `URI#file` is that the latter treats the argument + * as path, not as stringified-uri. E.g. `URI.file(path)` is **not the same as** + * `URI.parse('file://' + path)` because the path might contain characters that are + * interpreted (# and ?). See the following sample: + * ```ts + const good = URI.file('/coding/c#/project1'); + good.scheme === 'file'; + good.path === '/coding/c#/project1'; + good.fragment === ''; + const bad = URI.parse('file://' + '/coding/c#/project1'); + bad.scheme === 'file'; + bad.path === '/coding/c'; // path is now broken + bad.fragment === '/project1'; + ``` + * + * @param path A file system path (see `URI#fsPath`) + */ + static file(path: string): URI { + let authority = _empty; + + // normalize to fwd-slashes on windows, + // on other systems bwd-slashes are valid + // filename character, eg /f\oo/ba\r.txt + if (isWindows) { + path = path.replace(/\\/g, _slash); + } - } else if (path[0] !== _slash) { - // tricky -> makes invalid paths - // but otherwise we have to stop - // allowing relative paths... - path = _slash + path; + // check for authority as used in UNC shares + // or use the path as given + if (path[0] === _slash && path[1] === _slash) { + const idx = path.indexOf(_slash, 2); + if (idx === -1) { + authority = path.substring(2); + path = _slash; + } else { + authority = path.substring(2, idx); + path = path.substring(idx) || _slash; } - - return new _URI('file', authority, path, _empty, _empty); } - public 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, - ); - } + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return new _URI('file', authority, path, _empty, _empty); + } - // ---- printing/externalize --------------------------- + 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, + ); + } - /** - * - * @param skipEncoding Do not encode the result, default is `false` - */ - public toString(skipEncoding: boolean = false): string { - return _asFormatted(this, skipEncoding); - } + // ---- printing/externalize --------------------------- - public toJSON(): object { - const res = { - $mid: 1, - fsPath: this.fsPath, - external: this.toString(), - }; + /** + * 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); + } - if (this.path) { - res.path = this.path; - } + toJSON(): UriComponents { + return this; + } - if (this.scheme) { - res.scheme = this.scheme; - } + static revive(data: UriComponents | URI): URI; - if (this.authority) { - res.authority = this.authority; - } + static revive(data: UriComponents | URI | undefined): URI | undefined; - if (this.query) { - res.query = this.query; - } + static revive(data: UriComponents | URI | null): URI | null; - if (this.fragment) { - res.fragment = this.fragment; - } + static revive(data: UriComponents | URI | undefined | null): URI | undefined | null; - return res; + static revive(data: UriComponents | URI | undefined | null): URI | undefined | null { + if (!data) { + return data; } - - static revive(data: UriComponents | any): URI { - if (!data) { - return data; - } else if (data instanceof URI) { - return data; - } else { - let result = new _URI(data); - result._fsPath = (data).fsPath; - result._formatted = (data).external; - return result; - } + if (data instanceof URI) { + return data; } + // eslint-disable-next-line @typescript-eslint/no-use-before-define + const result = new _URI(data); + result._formatted = (data).external; + result._fsPath = (data)._sep === _pathSepMarker ? (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 }); } +} - interface UriState extends UriComponents { - $mid: number; - fsPath: string; - external: string; - } +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; +} - // tslint:disable-next-line:class-name - class _URI extends URI { +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; + } - _formatted: string = null; - _fsPath: string = null; + get fsPath(): string { + if (!this._fsPath) { + this._fsPath = _makeFsPath(this); + } + return this._fsPath; + } - get fsPath(): string { - if (!this._fsPath) { - this._fsPath = _makeFsPath(this); + 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); + } - public 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 = { + $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; } +} - - /** - * Compute `fsPath` for the given uri - * @param uri - */ - function _makeFsPath(uri: URI): string { - - let value: string; - if (uri.authority && uri.path && uri.scheme === 'file') { - // unc path: file://shares/c$/far/boo - value = `//${uri.authority}${uri.path}`; - } else if (_driveLetterPath.test(uri.path)) { - // windows drive letter: file:///c:/far/boo - value = uri.path[1].toLowerCase() + uri.path.substr(2); +// 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; + } + // check if we write into a new string (by default we try to return the param) + if (res !== undefined) { + res += uriComponent.charAt(pos); + } } else { - // other path - value = uri.path; - } - if (platform.isWindows) { - value = value.replace(/\//g, '\\'); + // 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; + } } - return value; } - /** - * Create the external version of a uri - */ - function _asFormatted(uri: URI, skipEncoding: boolean): string { - - const encoder = !skipEncoding - ? encodeURIComponent2 - : encodeNoop; - - const parts: string[] = []; - - let { scheme, authority, path, query, fragment } = uri; - if (scheme) { - parts.push(scheme, ':'); - } - if (authority || scheme === 'file') { - parts.push('//'); - } - if (authority) { - let idx = authority.indexOf('@'); - if (idx !== -1) { - const userinfo = authority.substr(0, idx); - authority = authority.substr(idx + 1); - idx = userinfo.indexOf(':'); - if (idx === -1) { - parts.push(encoder(userinfo)); - } else { - parts.push(encoder(userinfo.substr(0, idx)), ':', encoder(userinfo.substr(idx + 1))); - } - parts.push('@'); + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos)); + } + + 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); } - authority = authority.toLowerCase(); - idx = authority.indexOf(':'); + res += encodeTable[code]; + } else if (res !== undefined) { + res += path[pos]; + } + } + return res !== undefined ? res : path; +} + +/** + * Compute `fsPath` for the given uri + */ +function _makeFsPath(uri: URI): string { + let value: string; + if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') { + // unc path: file://shares/c$/far/boo + value = `//${uri.authority}${uri.path}`; + } else if ( + uri.path.charCodeAt(0) === CharCode.Slash && + ((uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z) || + (uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z)) && + uri.path.charCodeAt(2) === CharCode.Colon + ) { + // windows drive letter: file:///c:/far/boo + value = uri.path[1].toLowerCase() + uri.path.substr(2); + } else { + // other path + value = uri.path; + } + if (isWindows) { + value = value.replace(/\//g, '\\'); + } + return value; +} + +/** + * Create the external version of a uri + */ +function _asFormatted(uri: URI, skipEncoding: boolean): string { + const encoder = !skipEncoding ? encodeURIComponentFast : encodeURIComponentMinimal; + + let res = ''; + let { 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) { + // @ + const userinfo = authority.substr(0, idx); + authority = authority.substr(idx + 1); + idx = userinfo.indexOf(':'); if (idx === -1) { - parts.push(encoder(authority)); + res += encoder(userinfo, false); } else { - parts.push(encoder(authority.substr(0, idx)), authority.substr(idx)); + // :@ + res += encoder(userinfo.substr(0, idx), false); + res += ':'; + res += encoder(userinfo.substr(idx + 1), false); } + res += '@'; } - if (path) { - // lower-case windows drive letters in /C:/fff or C:/fff - const m = _upperCaseDrive.exec(path); - if (m) { - if (m[1]) { - path = '/' + m[2].toLowerCase() + path.substr(3); // "/c:".length === 3 - } else { - path = m[2].toLowerCase() + path.substr(2); // // "c:".length === 2 - } + authority = authority.toLowerCase(); + idx = authority.indexOf(':'); + if (idx === -1) { + res += encoder(authority, false); + } else { + // : + 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 } - - // encode every segement but not slashes - // make sure that # and ? are always encoded - // when occurring in paths - otherwise the result - // cannot be parsed back again - let lastIdx = 0; - while (true) { - let idx = path.indexOf(_slash, lastIdx); - if (idx === -1) { - parts.push(encoder(path.substring(lastIdx))); - break; - } - parts.push(encoder(path.substring(lastIdx, idx)), _slash); - lastIdx = idx + 1; + } 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 } } - if (query) { - parts.push('?', encoder(query)); - } - if (fragment) { - parts.push('#', encoder(fragment)); - } - - return parts.join(_empty); + // encode the rest of the path + res += encoder(path, true); + } + if (query) { + res += '?'; + res += encoder(query, false); + } + if (fragment) { + res += '#'; + res += !skipEncoding ? encodeURIComponentFast(fragment, false) : fragment; } + return res; } diff --git a/src/test/mocks/vsc/uuid.ts b/src/test/mocks/vsc/uuid.ts new file mode 100644 index 000000000000..fd825440ab7d --- /dev/null +++ b/src/test/mocks/vsc/uuid.ts @@ -0,0 +1,109 @@ +/* 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. + */ + +export interface UUID { + /** + * @returns the canonical representation in sets of hexadecimal numbers separated by dashes. + */ + asHex(): string; +} + +class ValueUUID implements UUID { + constructor(public _value: string) { + // empty + } + + public asHex(): string { + return this._value; + } +} + +class V4UUID extends ValueUUID { + private static readonly _chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; + + private static readonly _timeHighBits = ['8', '9', 'a', 'b']; + + private static _oneOf(array: string[]): string { + return array[Math.floor(array.length * Math.random())]; + } + + private static _randomHex(): string { + return V4UUID._oneOf(V4UUID._chars); + } + + constructor() { + super( + [ + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + '-', + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + '-', + '4', + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + '-', + V4UUID._oneOf(V4UUID._timeHighBits), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + '-', + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + ].join(''), + ); + } +} + +export function v4(): UUID { + return new V4UUID(); +} + +const _UUIDPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +export function isUUID(value: string): boolean { + return _UUIDPattern.test(value); +} + +/** + * Parses a UUID that is of the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. + * @param value A uuid string. + */ +export function parse(value: string): UUID { + if (!isUUID(value)) { + throw new Error('invalid uuid'); + } + + return new ValueUUID(value); +} + +export function generateUuid(): string { + return v4().asHex(); +} diff --git a/src/test/multiRootTest.ts b/src/test/multiRootTest.ts index 4e6e77c4034c..c8c63b6dabe5 100644 --- a/src/test/multiRootTest.ts +++ b/src/test/multiRootTest.ts @@ -1,13 +1,27 @@ -// tslint:disable:no-console no-require-imports no-var-requires - import * as path from 'path'; +import { runTests } from '@vscode/test-electron'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from './constants'; +import { initializeLogger } from './testLogger'; +import { getChannel } from './utils/vscode'; -process.env.CODE_TESTS_WORKSPACE = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); +const workspacePath = path.join(__dirname, '..', '..', 'src', 'testMultiRootWkspc', 'multi.code-workspace'); process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; +process.env.VSC_PYTHON_CI_TEST = '1'; + +initializeLogger(); function start() { console.log('*'.repeat(100)); console.log('Start Multiroot tests'); - require('../../node_modules/vscode/bin/test'); + runTests({ + extensionDevelopmentPath: EXTENSION_ROOT_DIR_FOR_TESTS, + extensionTestsPath: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'out', 'test', 'index'), + launchArgs: [workspacePath], + version: getChannel(), + extensionTestsEnv: { ...process.env, UITEST_DISABLE_INSIDERS: '1' }, + }).catch((ex) => { + console.error('End Multiroot tests (with errors)', ex); + process.exit(1); + }); } start(); diff --git a/src/test/multiRootWkspc/disableLinters/.vscode/tags b/src/test/multiRootWkspc/disableLinters/.vscode/tags 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 2bc223410653..5c90439e5546 100644 --- a/src/test/multiRootWkspc/multi.code-workspace +++ b/src/test/multiRootWkspc/multi.code-workspace @@ -23,8 +23,7 @@ "python.linting.pydocstyleEnabled": true, "python.linting.pylamaEnabled": true, "python.linting.pylintEnabled": false, - "python.linting.pep8Enabled": true, + "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 b78380782cd9..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 -} \ No newline at end of file +{} 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 b1dcdbc6b959..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'; @@ -18,7 +16,9 @@ const AllowedIncreaseInActivationDelayInMS = 500; suite('Activation Times', () => { if (process.env.ACTIVATION_TIMES_LOG_FILE_PATH) { const logFile = process.env.ACTIVATION_TIMES_LOG_FILE_PATH; - const sampleCounter = fs.existsSync(logFile) ? fs.readFileSync(logFile, { encoding: 'utf8' }).toString().split(/\r?\n/g).length : 1; + const sampleCounter = fs.existsSync(logFile) + ? fs.readFileSync(logFile, { encoding: 'utf8' }).toString().split(/\r?\n/g).length + : 1; if (sampleCounter > 5) { return; } @@ -39,35 +39,48 @@ suite('Activation Times', () => { }); } - if (process.env.ACTIVATION_TIMES_DEV_LOG_FILE_PATHS && + if ( + process.env.ACTIVATION_TIMES_DEV_LOG_FILE_PATHS && process.env.ACTIVATION_TIMES_RELEASE_LOG_FILE_PATHS && - process.env.ACTIVATION_TIMES_DEV_LANGUAGE_SERVER_LOG_FILE_PATHS) { - + process.env.ACTIVATION_TIMES_DEV_LANGUAGE_SERVER_LOG_FILE_PATHS + ) { test('Test activation times of Dev vs Release Extension', async () => { function getActivationTimes(files: string[]) { const activationTimes: number[] = []; for (const file of files) { - fs.readFileSync(file, { encoding: 'utf8' }).toString() + fs.readFileSync(file, { encoding: 'utf8' }) + .toString() .split(/\r?\n/g) - .map(line => line.trim()) - .filter(line => line.length > 0) - .map(line => parseInt(line, 10)) - .forEach(item => activationTimes.push(item)); + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .map((line) => parseInt(line, 10)) + .forEach((item) => activationTimes.push(item)); } return activationTimes; } const devActivationTimes = getActivationTimes(JSON.parse(process.env.ACTIVATION_TIMES_DEV_LOG_FILE_PATHS!)); - const releaseActivationTimes = getActivationTimes(JSON.parse(process.env.ACTIVATION_TIMES_RELEASE_LOG_FILE_PATHS!)); - const languageServerActivationTimes = getActivationTimes(JSON.parse(process.env.ACTIVATION_TIMES_DEV_LANGUAGE_SERVER_LOG_FILE_PATHS!)); - const devActivationAvgTime = devActivationTimes.reduce((sum, item) => sum + item, 0) / devActivationTimes.length; - const releaseActivationAvgTime = releaseActivationTimes.reduce((sum, item) => sum + item, 0) / releaseActivationTimes.length; - const languageServerActivationAvgTime = languageServerActivationTimes.reduce((sum, item) => sum + item, 0) / languageServerActivationTimes.length; + const releaseActivationTimes = getActivationTimes( + JSON.parse(process.env.ACTIVATION_TIMES_RELEASE_LOG_FILE_PATHS!), + ); + const languageServerActivationTimes = getActivationTimes( + JSON.parse(process.env.ACTIVATION_TIMES_DEV_LANGUAGE_SERVER_LOG_FILE_PATHS!), + ); + const devActivationAvgTime = + devActivationTimes.reduce((sum, item) => sum + item, 0) / devActivationTimes.length; + const releaseActivationAvgTime = + releaseActivationTimes.reduce((sum, item) => sum + item, 0) / releaseActivationTimes.length; + const languageServerActivationAvgTime = + languageServerActivationTimes.reduce((sum, item) => sum + item, 0) / + languageServerActivationTimes.length; console.log(`Dev version loaded in ${devActivationAvgTime}ms`); console.log(`Release version loaded in ${releaseActivationAvgTime}ms`); - console.log(`Language Server loaded in ${languageServerActivationAvgTime}ms`); + console.log(`Language server loaded in ${languageServerActivationAvgTime}ms`); - expect(devActivationAvgTime - releaseActivationAvgTime).to.be.lessThan(AllowedIncreaseInActivationDelayInMS, 'Activation times have increased above allowed threshold.'); + expect(devActivationAvgTime - releaseActivationAvgTime).to.be.lessThan( + AllowedIncreaseInActivationDelayInMS, + 'Activation times have increased above allowed threshold.', + ); }); } }); diff --git a/src/test/performance/settings.json b/src/test/performance/settings.json index 809ebf6ab2f4..ffc9d2a990cd 100644 --- a/src/test/performance/settings.json +++ b/src/test/performance/settings.json @@ -1 +1 @@ -{ "python.jediEnabled": true } +{ "python.languageServer": "Jedi" } diff --git a/src/test/performanceTest.ts b/src/test/performanceTest.ts index 8d7fe5e66678..2398f745c27a 100644 --- a/src/test/performanceTest.ts +++ b/src/test/performanceTest.ts @@ -12,18 +12,20 @@ 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'; +import { initializeLogger } from './testLogger'; + +initializeLogger(); const NamedRegexp = require('named-js-regexp'); const del = require('del'); @@ -33,7 +35,8 @@ const publishedExtensionPath = path.join(tmpFolder, 'ext', 'testReleaseExtension const logFilesPath = path.join(tmpFolder, 'test', 'logs'); enum Version { - Dev, Release + Dev, + Release, } class TestRunner { @@ -47,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`); @@ -58,59 +61,52 @@ 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.jediEnabled": ${!enable} }`; + private async enableLanguageServer() { + const settings = `{ "python.languageServer": "${LanguageServerType.Jedi}" }`; await fs.writeFile(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'performance', 'settings.json'), settings); } - private async capturePerfTimes(version: Version, logFile: string) { + private async capturePerfTimes(version: Version, logFile: string) { const releaseVersion = await this.getReleaseVersion(); const devVersion = await this.getDevVersion(); await fs.ensureDir(path.dirname(logFile)); - const env: { [key: string]: {} } = { + const env: Record = { 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); } - private async runPerfTest(devLogFiles: string[], releaseLogFiles: string[], languageServerLogFiles: string[]) { - const env: { [key: string]: {} } = { + private async runPerfTest(devLogFiles: string[], releaseLogFiles: string[], languageServerLogFiles: string[]) { + const env: Record = { 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: { [key: string]: {} }) { - await new Promise((resolve, reject) => { - const env: { [key: string]: {} } = { + private async launchTest(customEnvVars: Record) { + await new Promise((resolve, reject) => { + const env: Record = { 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 }); proc.stdout.pipe(process.stdout); proc.stderr.pipe(process.stderr); proc.on('error', reject); - proc.on('close', code => { + proc.on('close', (code) => { if (code === 0) { resolve(); } else { @@ -125,26 +121,17 @@ class TestRunner { await unzip(extensionFile, targetDir); } - private async getReleaseVersion(): Promise { + private async getReleaseVersion(): Promise { const url = `https://marketplace.visualstudio.com/items?itemName=${PVSC_EXTENSION_ID}`; - const content = await new Promise((resolve, reject) => { - request(url, (error, response, body) => { - if (error) { - return reject(error); - } - if (response.statusCode === 200) { - return resolve(body); - } - reject(`Status code of ${response.statusCode} received.`); - }); - }); - const re = NamedRegexp('"version"\S?:\S?"(:\\d{4}\\.\\d{1,2}\\.\\d{1,2})"', 'g'); + const request = bent.default('string', 'GET', 200); + + const content: string = await request(url); + const re = NamedRegexp('"version"S?:S?"(:\\d{4}\\.\\d{1,2}\\.\\d{1,2})"', 'g'); const matches = re.exec(content); return matches.groups().version; } - private async getDevVersion(): Promise { - // tslint:disable-next-line:non-literal-require + private async getDevVersion(): Promise { return require(path.join(EXTENSION_ROOT_DIR, 'package.json')).version; } @@ -156,9 +143,9 @@ 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; } } -new TestRunner().start().catch(ex => console.error('Error in running Performance Tests', ex)); +new TestRunner().start().catch((ex) => console.error('Error in running Performance Tests', ex)); diff --git a/src/test/proc.ts b/src/test/proc.ts new file mode 100644 index 000000000000..8a21eb379f76 --- /dev/null +++ b/src/test/proc.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as cp from 'child_process'; +import { sleep } from '../client/common/utils/async'; + +type OutStream = 'stdout' | 'stderr'; + +export class ProcOutput { + private readonly output: [OutStream, Buffer][] = []; + public get stdout(): string { + return this.dump('stdout'); + } + public get stderr(): string { + return this.dump('stderr'); + } + public get combined(): string { + return this.dump(); + } + public addStdout(data: Buffer) { + this.output.push(['stdout', data]); + } + public addStderr(data: Buffer) { + this.output.push(['stdout', data]); + } + private dump(which?: OutStream) { + let out = ''; + for (const [stream, data] of this.output) { + if (!which || which !== stream) { + continue; + } + out += data.toString(); + } + return out; + } +} + +export type ProcResult = { + exitCode: number; + stdout: string; +}; + +interface IRawProc extends cp.ChildProcess { + // Apparently the type declaration doesn't expose exitCode. + // See: https://nodejs.org/api/child_process.html#child_process_subprocess_exitcode + exitCode: number | null; +} + +export class Proc { + public readonly raw: IRawProc; + private readonly output: ProcOutput; + private result: ProcResult | undefined; + constructor(raw: cp.ChildProcess, output: ProcOutput) { + this.raw = (raw as unknown) as IRawProc; + this.output = output; + } + public get pid(): number | undefined { + return this.raw.pid; + } + public get exited(): boolean { + return this.raw.exitCode !== null; + } + public async waitUntilDone(): Promise { + if (this.result) { + return this.result; + } + while (this.raw.exitCode === null) { + await sleep(10); // milliseconds + } + this.result = { + exitCode: this.raw.exitCode, + stdout: this.output.stdout, + }; + return this.result; + } +} + +export function spawn(executable: string, ...args: string[]) { + // Un-comment this to see the executed command: + //console.log(`|${executable} ${args.join(' ')}|`); + const output = new ProcOutput(); + const raw = cp.spawn(executable, args); + raw.stdout.on('data', (data: Buffer) => output.addStdout(data)); + raw.stderr.on('data', (data: Buffer) => output.addStderr(data)); + return new Proc(raw, output); +} diff --git a/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts b/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts new file mode 100644 index 000000000000..136271b3e4e5 --- /dev/null +++ b/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { CodeActionContext, CodeActionKind, Diagnostic, Range, TextDocument, Uri } from 'vscode'; +import { LaunchJsonCodeActionProvider } from '../../../client/providers/codeActionProvider/launchJsonCodeActionProvider'; + +suite('LaunchJson CodeAction Provider', () => { + const documentUri = Uri.parse('a'); + let document: TypeMoq.IMock; + let range: TypeMoq.IMock; + let context: TypeMoq.IMock; + let diagnostic: TypeMoq.IMock; + let codeActionsProvider: LaunchJsonCodeActionProvider; + + setup(() => { + codeActionsProvider = new LaunchJsonCodeActionProvider(); + document = TypeMoq.Mock.ofType(); + range = TypeMoq.Mock.ofType(); + context = TypeMoq.Mock.ofType(); + diagnostic = TypeMoq.Mock.ofType(); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => 'Diagnostic text'); + document.setup((d) => d.uri).returns(() => documentUri); + context.setup((c) => c.diagnostics).returns(() => [diagnostic.object]); + }); + + test('Ensure correct code action is returned if diagnostic message equals `Incorrect type. Expected "string".`', async () => { + diagnostic.setup((d) => d.message).returns(() => 'Incorrect type. Expected "string".'); + diagnostic.setup((d) => d.range).returns(() => new Range(2, 0, 7, 8)); + + const codeActions = codeActionsProvider.provideCodeActions(document.object, range.object, context.object); + + // Now ensure that the code action object is as expected + expect(codeActions).to.have.length(1); + expect(codeActions[0].kind).to.eq(CodeActionKind.QuickFix); + expect(codeActions[0].title).to.equal('Convert to "Diagnostic text"'); + + // Ensure the correct TextEdit is provided + const entries = codeActions[0].edit!.entries(); + // Edits the correct document is edited + assert.deepEqual(entries[0][0], documentUri); + const edit = entries[0][1][0]; + // Final text is as expected + expect(edit.newText).to.equal('"Diagnostic text"'); + // Text edit range is as expected + expect(edit.range.isEqual(new Range(2, 0, 7, 8))).to.equal(true, 'Text edit range not as expected'); + }); + + test('Ensure no code action is returned if diagnostic message does not equal `Incorrect type. Expected "string".`', async () => { + diagnostic.setup((d) => d.message).returns(() => 'Random diagnostic message'); + + const codeActions = codeActionsProvider.provideCodeActions(document.object, range.object, context.object); + + expect(codeActions).to.have.length(0); + }); +}); diff --git a/src/test/providers/codeActionProvider/main.unit.test.ts b/src/test/providers/codeActionProvider/main.unit.test.ts new file mode 100644 index 000000000000..55644d80ae54 --- /dev/null +++ b/src/test/providers/codeActionProvider/main.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 rewiremock from 'rewiremock'; +import * as typemoq from 'typemoq'; +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'; + +suite('Code Action Provider service', async () => { + setup(() => { + rewiremock.disable(); + }); + test('Code actions are registered correctly', async () => { + let selector: DocumentSelector; + let provider: CodeActionProvider; + let metadata: CodeActionProviderMetadata; + const vscodeMock = { + languages: { + registerCodeActionsProvider: ( + _selector: DocumentSelector, + _provider: CodeActionProvider, + _metadata: CodeActionProviderMetadata, + ) => { + selector = _selector; + provider = _provider; + metadata = _metadata; + }, + }, + CodeActionKind: { + QuickFix: 'CodeAction', + }, + }; + rewiremock.enable(); + rewiremock('vscode').with(vscodeMock); + const quickFixService = new CodeActionProviderService(typemoq.Mock.ofType().object); + + await quickFixService.activate(); + + // Ensure QuickFixLaunchJson is registered with correct arguments + assert.deepEqual(selector!, { + scheme: 'file', + language: 'jsonc', + pattern: '**/launch.json', + }); + assert.deepEqual(metadata!, { + providedCodeActionKinds: [('CodeAction' as unknown) as CodeActionKind], + }); + expect(provider!).instanceOf(LaunchJsonCodeActionProvider); + }); +}); diff --git a/src/test/providers/codeActionsProvider.test.ts b/src/test/providers/codeActionsProvider.test.ts deleted file mode 100644 index 8147063d649e..000000000000 --- a/src/test/providers/codeActionsProvider.test.ts +++ /dev/null @@ -1,44 +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 { CancellationToken, CodeActionContext, CodeActionKind, Range, TextDocument } from 'vscode'; -import { PythonCodeActionProvider } from '../../client/providers/codeActionsProvider'; - -suite('CodeAction Provider', () => { - let codeActionsProvider: PythonCodeActionProvider; - let document: TypeMoq.IMock; - let range: TypeMoq.IMock; - let context: TypeMoq.IMock; - let token: TypeMoq.IMock; - - setup(() => { - codeActionsProvider = new PythonCodeActionProvider(); - document = TypeMoq.Mock.ofType(); - range = TypeMoq.Mock.ofType(); - context = TypeMoq.Mock.ofType(); - token = TypeMoq.Mock.ofType(); - }); - - test('Ensure it always returns a source.organizeImports CodeAction', async () => { - const codeActions = await codeActionsProvider.provideCodeActions( - document.object, - range.object, - context.object, - token.object - ); - - if (!codeActions) { - throw Error(`codeActionsProvider.provideCodeActions did not return an array (it returned ${codeActions})`); - } - - const organizeImportsCodeAction = codeActions.filter( - codeAction => codeAction.kind === CodeActionKind.SourceOrganizeImports - ); - expect(organizeImportsCodeAction).to.have.length(1); - expect(organizeImportsCodeAction[0].kind).to.eq(CodeActionKind.SourceOrganizeImports); - }); -}); diff --git a/src/test/providers/foldingProvider.test.ts b/src/test/providers/foldingProvider.test.ts deleted file mode 100644 index 9de189afb47f..000000000000 --- a/src/test/providers/foldingProvider.test.ts +++ /dev/null @@ -1,65 +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(95, 143), new FoldingRange(149, 150, FoldingRangeKind.Comment), - new FoldingRange(305, 313), new FoldingRange(320, 322) - ] - }, - { - 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 7c30ff82d6cf..000000000000 --- a/src/test/providers/importSortProvider.unit.test.ts +++ /dev/null @@ -1,368 +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 { EOL } from 'os'; -import * as path from 'path'; -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 { IFileSystem, TemporaryFile } from '../../client/common/platform/types'; -import { ProcessService } from '../../client/common/process/proc'; -import { IProcessServiceFactory, IPythonExecutionFactory, IPythonExecutionService } from '../../client/common/process/types'; -import { IConfigurationService, IDisposableRegistry, IEditorUtils, IPythonSettings, ISortImportSettings } from '../../client/common/types'; -import { noop } from '../../client/common/utils/misc'; -import { IServiceContainer } from '../../client/ioc/types'; -import { SortImportsEditingProvider } from '../../client/providers/importSortProvider'; -import { ISortImportsEditingProvider } from '../../client/providers/types'; - -suite('Import Sort Provider', () => { - let serviceContainer: TypeMoq.IMock; - let shell: TypeMoq.IMock; - let documentManager: TypeMoq.IMock; - let configurationService: TypeMoq.IMock; - let pythonExecFactory: TypeMoq.IMock; - let processServiceFactory: TypeMoq.IMock; - let editorUtils: TypeMoq.IMock; - let commandManager: TypeMoq.IMock; - let pythonSettings: TypeMoq.IMock; - let sortProvider: ISortImportsEditingProvider; - let fs: TypeMoq.IMock; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType(); - commandManager = TypeMoq.Mock.ofType(); - fs = TypeMoq.Mock.ofType(); - documentManager = TypeMoq.Mock.ofType(); - shell = TypeMoq.Mock.ofType(); - configurationService = TypeMoq.Mock.ofType(); - pythonExecFactory = TypeMoq.Mock.ofType(); - processServiceFactory = TypeMoq.Mock.ofType(); - pythonSettings = TypeMoq.Mock.ofType(); - editorUtils = TypeMoq.Mock.ofType(); - fs = TypeMoq.Mock.ofType(); - 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(() => []); - serviceContainer.setup(c => c.get(IFileSystem)).returns(() => fs.object); - 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(); - const mockDoc = TypeMoq.Mock.ofType(); - 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(); - // 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(); - // 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(); - mockDoc.setup((d: any) => d.then).returns(() => undefined); - mockDoc.setup(d => d.lineCount) - .returns(() => 10) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const lastLine = TypeMoq.Mock.ofType(); - 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(); - 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(); - 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 temporary file is created for sorting when document is dirty', async () => { - const uri = Uri.file('something.py'); - const mockDoc = TypeMoq.Mock.ofType(); - let tmpFileDisposed = false; - const tmpFile: TemporaryFile = { filePath: 'TmpFile', dispose: () => tmpFileDisposed = true }; - const processService = TypeMoq.Mock.ofType(); - 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.atLeastOnce()); - 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()); - fs.setup(f => f.createTemporaryFile(TypeMoq.It.isValue('.py'))) - .returns(() => Promise.resolve(tmpFile)) - .verifiable(TypeMoq.Times.once()); - fs.setup(f => f.writeFile(TypeMoq.It.isValue(tmpFile.filePath), TypeMoq.It.isValue('Hello'))) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - 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()); - - const expectedArgs = [tmpFile.filePath, '--diff', '1', '2']; - processService - .setup(p => p.exec(TypeMoq.It.isValue('CUSTOM_ISORT'), TypeMoq.It.isValue(expectedArgs), TypeMoq.It.isValue({ throwOnStdErr: true, token: undefined }))) - .returns(() => Promise.resolve({ stdout: 'DIFF' })) - .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); - expect(tmpFileDisposed).to.be.equal(true, 'Temporary file not disposed'); - shell.verifyAll(); - documentManager.verifyAll(); - }); - test('Ensure temporary file is created for sorting when document is dirty (with custom isort path)', async () => { - const uri = Uri.file('something.py'); - const mockDoc = TypeMoq.Mock.ofType(); - let tmpFileDisposed = false; - const tmpFile: TemporaryFile = { filePath: 'TmpFile', dispose: () => tmpFileDisposed = true }; - const processService = TypeMoq.Mock.ofType(); - 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.atLeastOnce()); - 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()); - fs.setup(f => f.createTemporaryFile(TypeMoq.It.isValue('.py'))) - .returns(() => Promise.resolve(tmpFile)) - .verifiable(TypeMoq.Times.once()); - fs.setup(f => f.writeFile(TypeMoq.It.isValue(tmpFile.filePath), TypeMoq.It.isValue('Hello'))) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - pythonSettings.setup(s => s.sortImports) - .returns(() => { return { args: ['1', '2'] } as any as ISortImportSettings; }) - .verifiable(TypeMoq.Times.once()); - - const processExeService = TypeMoq.Mock.ofType(); - 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()); - const importScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'sortImports.py'); - const expectedArgs = [importScript, tmpFile.filePath, '--diff', '1', '2']; - processExeService - .setup(p => p.exec(TypeMoq.It.isValue(expectedArgs), TypeMoq.It.isValue({ throwOnStdErr: true, token: undefined }))) - .returns(() => Promise.resolve({ stdout: 'DIFF' })) - .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); - expect(tmpFileDisposed).to.be.equal(true, 'Temporary file not disposed'); - shell.verifyAll(); - documentManager.verifyAll(); - }); -}); diff --git a/src/test/providers/repl.unit.test.ts b/src/test/providers/repl.unit.test.ts index 784a07868d71..72adfa95a4a0 100644 --- a/src/test/providers/repl.unit.test.ts +++ b/src/test/providers/repl.unit.test.ts @@ -3,134 +3,105 @@ import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; -import { Disposable, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +import { Disposable, Uri } from 'vscode'; +import { + IActiveResourceService, + ICommandManager, + IDocumentManager, + 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; let commandManager: TypeMoq.IMock; let workspace: TypeMoq.IMock; let codeExecutionService: TypeMoq.IMock; let documentManager: TypeMoq.IMock; + let activeResourceService: TypeMoq.IMock; let replProvider: ReplProvider; + let interpreterService: TypeMoq.IMock; setup(() => { serviceContainer = TypeMoq.Mock.ofType(); commandManager = TypeMoq.Mock.ofType(); workspace = TypeMoq.Mock.ofType(); codeExecutionService = TypeMoq.Mock.ofType(); documentManager = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(ICommandManager)).returns(() => commandManager.object); - serviceContainer.setup(c => c.get(IWorkspaceService)).returns(() => workspace.object); - serviceContainer.setup(c => c.get(ICodeExecutionService, TypeMoq.It.isValue('repl'))).returns(() => codeExecutionService.object); - serviceContainer.setup(c => c.get(IDocumentManager)).returns(() => documentManager.object); + activeResourceService = TypeMoq.Mock.ofType(); + serviceContainer.setup((c) => c.get(ICommandManager)).returns(() => commandManager.object); + serviceContainer.setup((c) => c.get(IWorkspaceService)).returns(() => workspace.object); + serviceContainer + .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(); + 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()); + commandManager.verify( + (c) => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); test('Ensure command handler is disposed', () => { const disposable = TypeMoq.Mock.ofType(); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => disposable.object); + commandManager + .setup((c) => + c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + ) + .returns(() => disposable.object); replProvider = new ReplProvider(serviceContainer.object); replProvider.dispose(); - disposable.verify(d => d.dispose(), TypeMoq.Times.once()); + disposable.verify((d) => d.dispose(), TypeMoq.Times.once()); }); - test('Ensure resource is \'undefined\' if there\s no active document nor a workspace', () => { + 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(); - let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; - }); - documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + let commandHandler: undefined | (() => Promise); + + commandManager + .setup((c) => + c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + ) + .returns((_cmd, callback) => { + commandHandler = callback; + return disposable.object; + }); + activeResourceService + .setup((a) => a.getActiveResource()) + .returns(() => resource) + .verifiable(TypeMoq.Times.once()); replProvider = new ReplProvider(serviceContainer.object); expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); - commandHandler!.call(replProvider); + await commandHandler!.call(replProvider); - serviceContainer.verify(c => c.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('repl')), TypeMoq.Times.once()); - codeExecutionService.verify(c => c.initializeRepl(TypeMoq.It.isValue(undefined)), TypeMoq.Times.once()); - }); - - test('Ensure resource is uri of the active document', () => { - const disposable = TypeMoq.Mock.ofType(); - let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; - }); - const documentUri = Uri.file('a'); - const editor = TypeMoq.Mock.ofType(); - const document = TypeMoq.Mock.ofType(); - document.setup(d => d.uri).returns(() => documentUri); - document.setup(d => d.isUntitled).returns(() => false); - editor.setup(e => e.document).returns(() => document.object); - documentManager.setup(d => d.activeTextEditor).returns(() => editor.object); - - replProvider = new ReplProvider(serviceContainer.object); - expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); - commandHandler!.call(replProvider); - - serviceContainer.verify(c => c.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('repl')), TypeMoq.Times.once()); - codeExecutionService.verify(c => c.initializeRepl(TypeMoq.It.isValue(documentUri)), TypeMoq.Times.once()); - }); - - test('Ensure resource is \'undefined\' if the active document is not used if it is untitled (new document)', () => { - const disposable = TypeMoq.Mock.ofType(); - let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; - }); - const editor = TypeMoq.Mock.ofType(); - const document = TypeMoq.Mock.ofType(); - document.setup(d => d.isUntitled).returns(() => true); - editor.setup(e => e.document).returns(() => document.object); - documentManager.setup(d => d.activeTextEditor).returns(() => editor.object); - - replProvider = new ReplProvider(serviceContainer.object); - expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); - commandHandler!.call(replProvider); - - serviceContainer.verify(c => c.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('repl')), TypeMoq.Times.once()); - codeExecutionService.verify(c => c.initializeRepl(TypeMoq.It.isValue(undefined)), TypeMoq.Times.once()); - }); - - test('Ensure first available workspace folder is used if there no document', () => { - const disposable = TypeMoq.Mock.ofType(); - let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; - }); - documentManager.setup(d => d.activeTextEditor).returns(() => undefined); - - const workspaceUri = Uri.file('a'); - const workspaceFolder = TypeMoq.Mock.ofType(); - workspaceFolder.setup(w => w.uri).returns(() => workspaceUri); - workspace.setup(w => w.workspaceFolders).returns(() => [workspaceFolder.object]); - - replProvider = new ReplProvider(serviceContainer.object); - expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); - commandHandler!.call(replProvider); - - serviceContainer.verify(c => c.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('repl')), TypeMoq.Times.once()); - codeExecutionService.verify(c => c.initializeRepl(TypeMoq.It.isValue(workspaceUri)), TypeMoq.Times.once()); + serviceContainer.verify( + (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 new file mode 100644 index 000000000000..007638ab77b6 --- /dev/null +++ b/src/test/providers/serviceRegistry.unit.test.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { instance, mock, verify } from 'ts-mockito'; +import { IExtensionSingleActivationService } from '../../client/activation/types'; +import { ServiceManager } from '../../client/ioc/serviceManager'; +import { IServiceManager } from '../../client/ioc/types'; +import { CodeActionProviderService } from '../../client/providers/codeActionProvider/main'; +import { registerTypes } from '../../client/providers/serviceRegistry'; + +suite('Common Providers Service Registry', () => { + let serviceManager: IServiceManager; + + setup(() => { + serviceManager = mock(ServiceManager); + }); + + test('Ensure services are registered', async () => { + registerTypes(instance(serviceManager)); + verify( + serviceManager.addSingleton( + IExtensionSingleActivationService, + 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 fe4967354d41..000000000000 --- a/src/test/providers/shebangCodeLenseProvider.unit.test.ts +++ /dev/null @@ -1,170 +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; - let workspaceService: IWorkspaceService; - let provider: ShebangCodeLensProvider; - let factory: IProcessServiceFactory; - let processService: typemoq.IMock; - let platformService: typemoq.IMock; - setup(() => { - pythonSettings = typemoq.Mock.ofType(); - configurationService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - factory = mock(ProcessServiceFactory); - processService = typemoq.Mock.ofType(); - platformService = typemoq.Mock.ofType(); - // 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, typemoq.IMock] { - const doc = typemoq.Mock.ofType(); - const line = typemoq.Mock.ofType(); - - 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', async () => { - const [document, line] = createDocument(''); - - const shebang = await provider.detectShebang(document.object); - - document.verifyAll(); - line.verifyAll(); - expect(shebang).to.be.equal(undefined, 'Shebang should be undefined'); - }); - 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); - - 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); - - 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); - - 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); - - 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'); - 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'); - 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'); - 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'); - 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 6573ef9d231a..8f684835b7cf 100644 --- a/src/test/providers/terminal.unit.test.ts +++ b/src/test/providers/terminal.unit.test.ts @@ -1,152 +1,250 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // 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, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../client/common/application/types'; +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 { ITerminalServiceFactory } from '../../client/common/terminal/types'; +import { ITerminalActivator, ITerminalServiceFactory } from '../../client/common/terminal/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; let commandManager: TypeMoq.IMock; let workspace: TypeMoq.IMock; - let documentManager: TypeMoq.IMock; + let activeResourceService: TypeMoq.IMock; + let experimentService: TypeMoq.IMock; 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(); commandManager = TypeMoq.Mock.ofType(); + experimentService = TypeMoq.Mock.ofType(); + experimentService.setup((e) => e.inExperimentSync(TerminalEnvVarActivation.experiment)).returns(() => false); + activeResourceService = TypeMoq.Mock.ofType(); workspace = TypeMoq.Mock.ofType(); - documentManager = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(ICommandManager)).returns(() => commandManager.object); - serviceContainer.setup(c => c.get(IWorkspaceService)).returns(() => workspace.object); - serviceContainer.setup(c => c.get(IDocumentManager)).returns(() => documentManager.object); + 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', () => { terminalProvider = new TerminalProvider(serviceContainer.object); - commandManager.verify(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + commandManager.verify( + (c) => + c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); test('Ensure command handler is disposed', () => { const disposable = TypeMoq.Mock.ofType(); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => disposable.object); + commandManager + .setup((c) => + c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + ) + .returns(() => disposable.object); terminalProvider = new TerminalProvider(serviceContainer.object); terminalProvider.dispose(); - disposable.verify(d => d.dispose(), TypeMoq.Times.once()); + disposable.verify((d) => d.dispose(), TypeMoq.Times.once()); }); test('Ensure terminal is created and displayed when command is invoked', () => { const disposable = TypeMoq.Mock.ofType(); let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; - }); - documentManager.setup(d => d.activeTextEditor).returns(() => undefined); - workspace.setup(w => w.workspaceFolders).returns(() => undefined); + commandManager + .setup((c) => + c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + ) + .returns((_cmd, callback) => { + commandHandler = callback; + return disposable.object; + }); + activeResourceService + .setup((a) => a.getActiveResource()) + .returns(() => resource) + .verifiable(TypeMoq.Times.once()); + workspace.setup((w) => w.workspaceFolders).returns(() => undefined); terminalProvider = new TerminalProvider(serviceContainer.object); expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); const terminalServiceFactory = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))) + .returns(() => terminalServiceFactory.object); const terminalService = TypeMoq.Mock.ofType(); - terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(undefined), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object); + terminalServiceFactory + .setup((t) => t.createTerminalService(TypeMoq.It.isValue(resource), TypeMoq.It.isValue('Python'))) + .returns(() => terminalService.object); commandHandler!.call(terminalProvider); - terminalService.verify(t => t.show(false), TypeMoq.Times.once()); + activeResourceService.verifyAll(); + terminalService.verify((t) => t.show(false), TypeMoq.Times.once()); }); - test('Ensure terminal creation does not use uri of the active documents which is untitled', () => { - const disposable = TypeMoq.Mock.ofType(); - let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; + suite('terminal.activateCurrentTerminal setting', () => { + let pythonSettings: TypeMoq.IMock; + let terminalSettings: TypeMoq.IMock; + let configService: TypeMoq.IMock; + let terminalActivator: TypeMoq.IMock; + let terminal: TypeMoq.IMock; + + setup(() => { + configService = TypeMoq.Mock.ofType(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + pythonSettings = TypeMoq.Mock.ofType(); + activeResourceService = TypeMoq.Mock.ofType(); + + terminalSettings = TypeMoq.Mock.ofType(); + pythonSettings.setup((s) => s.terminal).returns(() => terminalSettings.object); + + terminalActivator = TypeMoq.Mock.ofType(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalActivator))) + .returns(() => terminalActivator.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IActiveResourceService))) + .returns(() => activeResourceService.object); + + terminal = TypeMoq.Mock.ofType(); + terminal.setup((c) => c.creationOptions).returns(() => ({ hideFromUser: false })); }); - const editor = TypeMoq.Mock.ofType(); - documentManager.setup(d => d.activeTextEditor).returns(() => editor.object); - const document = TypeMoq.Mock.ofType(); - document.setup(d => d.isUntitled).returns(() => true); - editor.setup(e => e.document).returns(() => document.object); - workspace.setup(w => w.workspaceFolders).returns(() => undefined); - terminalProvider = new TerminalProvider(serviceContainer.object); - expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); - - const terminalServiceFactory = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object); - const terminalService = TypeMoq.Mock.ofType(); - terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(undefined), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object); - - commandHandler!.call(terminalProvider); - terminalService.verify(t => t.show(false), TypeMoq.Times.once()); - }); - - test('Ensure terminal creation uses uri of active document', () => { - const disposable = TypeMoq.Mock.ofType(); - let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; + test('If terminal.activateCurrentTerminal setting is set, provided terminal should be activated', async () => { + terminalSettings.setup((t) => t.activateEnvInCurrentTerminal).returns(() => true); + configService + .setup((c) => c.getSettings(resource)) + .returns(() => pythonSettings.object) + .verifiable(TypeMoq.Times.once()); + activeResourceService + .setup((a) => a.getActiveResource()) + .returns(() => resource) + .verifiable(TypeMoq.Times.once()); + + terminalProvider = new TerminalProvider(serviceContainer.object); + await terminalProvider.initialize(terminal.object); + + terminalActivator.verify( + (a) => a.activateEnvironmentInTerminal(terminal.object, TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); + configService.verifyAll(); + activeResourceService.verifyAll(); }); - const editor = TypeMoq.Mock.ofType(); - documentManager.setup(d => d.activeTextEditor).returns(() => editor.object); - const document = TypeMoq.Mock.ofType(); - const documentUri = Uri.file('a'); - document.setup(d => d.isUntitled).returns(() => false); - document.setup(d => d.uri).returns(() => documentUri); - editor.setup(e => e.document).returns(() => document.object); - workspace.setup(w => w.workspaceFolders).returns(() => undefined); - - terminalProvider = new TerminalProvider(serviceContainer.object); - expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); - const terminalServiceFactory = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object); - const terminalService = TypeMoq.Mock.ofType(); - terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(documentUri), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object); - - commandHandler!.call(terminalProvider); - terminalService.verify(t => t.show(false), TypeMoq.Times.once()); - }); - - test('Ensure terminal creation uses uri of active workspace', () => { - const disposable = TypeMoq.Mock.ofType(); - let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; + test('If terminal.activateCurrentTerminal setting is not set, provided terminal should not be activated', async () => { + terminalSettings.setup((t) => t.activateEnvInCurrentTerminal).returns(() => false); + configService + .setup((c) => c.getSettings(resource)) + .returns(() => pythonSettings.object) + .verifiable(TypeMoq.Times.once()); + activeResourceService + .setup((a) => a.getActiveResource()) + .returns(() => resource) + .verifiable(TypeMoq.Times.once()); + + terminalProvider = new TerminalProvider(serviceContainer.object); + await terminalProvider.initialize(terminal.object); + + terminalActivator.verify( + (a) => a.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never(), + ); + activeResourceService.verifyAll(); + configService.verifyAll(); }); - documentManager.setup(d => d.activeTextEditor).returns(() => undefined); - const workspaceUri = Uri.file('a'); - const workspaceFolder = TypeMoq.Mock.ofType(); - workspaceFolder.setup(w => w.uri).returns(() => workspaceUri); - workspace.setup(w => w.workspaceFolders).returns(() => [workspaceFolder.object]); - terminalProvider = new TerminalProvider(serviceContainer.object); - expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); + test('If terminal.activateCurrentTerminal setting is set, but hideFromUser is true, provided terminal should not be activated', async () => { + terminalSettings.setup((t) => t.activateEnvInCurrentTerminal).returns(() => true); + configService + .setup((c) => c.getSettings(resource)) + .returns(() => pythonSettings.object) + .verifiable(TypeMoq.Times.once()); + activeResourceService + .setup((a) => a.getActiveResource()) + .returns(() => resource) + .verifiable(TypeMoq.Times.once()); + + terminal.setup((c) => c.creationOptions).returns(() => ({ hideFromUser: true })); + + terminalProvider = new TerminalProvider(serviceContainer.object); + await terminalProvider.initialize(terminal.object); + + terminalActivator.verify( + (a) => a.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never(), + ); + activeResourceService.verifyAll(); + configService.verifyAll(); + }); - const terminalServiceFactory = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object); - const terminalService = TypeMoq.Mock.ofType(); - terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(workspaceUri), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object); + test('terminal.activateCurrentTerminal setting is set but provided terminal is undefined', async () => { + terminalSettings.setup((t) => t.activateEnvInCurrentTerminal).returns(() => true); + configService + .setup((c) => c.getSettings(resource)) + .returns(() => pythonSettings.object) + .verifiable(TypeMoq.Times.once()); + activeResourceService + .setup((a) => a.getActiveResource()) + .returns(() => resource) + .verifiable(TypeMoq.Times.once()); + + terminalProvider = new TerminalProvider(serviceContainer.object); + await terminalProvider.initialize(undefined); + + terminalActivator.verify( + (a) => a.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never(), + ); + activeResourceService.verifyAll(); + configService.verifyAll(); + }); - commandHandler!.call(terminalProvider); - terminalService.verify(t => t.show(false), TypeMoq.Times.once()); + test('Exceptions are swallowed if initializing terminal provider fails', async () => { + terminalSettings.setup((t) => t.activateEnvInCurrentTerminal).returns(() => true); + configService.setup((c) => c.getSettings(resource)).throws(new Error('Kaboom')); + activeResourceService.setup((a) => a.getActiveResource()).returns(() => resource); + + terminalProvider = new TerminalProvider(serviceContainer.object); + try { + await terminalProvider.initialize(undefined); + } catch (ex) { + assert.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 extends Locator { + public readonly providerId: string = 'SimpleLocator'; + + private deferred = createDeferred(); + + constructor( + private envs: I[], + public callbacks: { + resolve?: null | ((env: PythonEnvInfo | string) => Promise); + before?(): Promise; + after?(): Promise; + onUpdated?: Event | ProgressNotificationEvent>; + beforeEach?(e: I): Promise; + afterEach?(e: I): Promise; + onQuery?(query: PythonLocatorQuery | undefined, envs: I[]): Promise; + } = {}, + private options?: { resolveAsString?: boolean }, + ) { + super(); + } + + public get done(): Promise { + return this.deferred.promise; + } + + public fire(event: PythonEnvsChangedEvent): void { + this.emitter.fire(event); + } + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + const { deferred } = this; + const { callbacks } = this; + let { envs } = this; + const iterator: IPythonEnvsIterator = (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 { + 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(iterator: IPythonEnvsIterator): Promise { + return flattenIterator(iterator); +} + +/** + * Unroll the given iterator into an array. + * + * This includes applying any received updates. + */ +export async function getEnvsWithUpdates( + iterator: IPythonEnvsIterator, + iteratorUpdateCallback: () => void = noop, +): Promise { + const envs: (I | undefined)[] = []; + + const updatesDone = createDeferred(); + 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>((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[] = []; + 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[] = []; + 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(); + // 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + const deferred2 = createDeferred(); + const deferred4 = createDeferred(); + const deferred5 = createDeferred(); + 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 { + throw new Error('Method not implemented.'); + } + + getCondaInfo(): Promise { + throw new Error('Method not implemented.'); + } + + resolve(_executable: string): Promise { + throw new Error('Method not implemented.'); + } + + refresh(): AsyncIterable { + 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(); + 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(); + 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(); + 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(); + 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(); + const locatedEnvs = getLocatorEnvs(); + const cachedEnvs = getCachedEnvs(); + const waitUntilEventVerified = createDeferred(); + const waitForAllPathsDiscoveredEvent = createDeferred(); + 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(); + const waitDeferred = createDeferred(); + 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(envsReturnedByParentLocator); + const reducer = new PythonEnvsReducer(parentLocator); + + // Act + const iterator = reducer.iterEnvs(); + let stage: ProgressReportStage | undefined; + let waitForProgressEvent = createDeferred(); + 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(); + 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>((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(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(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>((resolve) => { + resolve({ + stdout: '', + }); + }), + ); + // Arrange + const env1 = createBasicEnv( + PythonEnvKind.Venv, + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const envsReturnedByParentLocator = [env1]; + const parentLocator = new SimpleLocator(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 | ProgressNotificationEvent>(); + const parentLocator = new SimpleLocator(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(); + stubShellExec.reset(); + stubShellExec.returns( + shellExecDeferred.promise.then( + () => + new Promise>((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 | ProgressNotificationEvent>(); + const parentLocator = new SimpleLocator(envsReturnedByParentLocator, { + onUpdated: didUpdate.event, + }); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const iterator = resolver.iterEnvs(); + let stage: ProgressReportStage | undefined; + let waitForProgressEvent = createDeferred(); + 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(); + 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(); + 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>((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>((_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>((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 { + 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 { + 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 { + 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 { + await fsapi.writeFile(filename, text); + return filename; +} + +export async function deleteFile(filename: string): Promise { + 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>({ + 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 { + try { + await fs.createFile(this.condaEnvironmentsTxt); + } catch (err) { + throw new Error(`Failed to create environments.txt ${this.condaEnvironmentsTxt}, Error: ${err}`); + } + } + + public async update(): Promise { + 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) { + 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) { + 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(); + 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(); + 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(); + 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 { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line class-methods-use-this + protected doResolveEnv(): Promise { + 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 { + 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 { + // 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) { + const timeout = setTimeout(() => { + clearTimeout(timeout); + deferred.reject(new Error('Environment not detected')); + }, TEST_TIMEOUT); + await deferred.promise; + } + + async function isLocated(executable: string): Promise { + 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) { + 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(); + 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(); + 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(); + 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>({ stdout: '' }); + } + if (command.indexOf('python3.7.exe') > 0) { + return Promise.resolve>({ stdout: JSON.stringify(python379data) }); + } + return Promise.resolve>({ 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>({ + 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>({ + 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 = 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 { + 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 { + try { + await fs.remove(filename); + } catch (err) { + traceWarn(`Failed to clean up ${filename}`); + } + } + + public async cleanUp(): Promise { + 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 & IDisposable>; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type locatorFactoryFuncType2 = (_: any) => Promise & 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 & IDisposable; + const venvs = new Venvs(root); + + async function waitForChangeToBeDetected(deferred: Deferred) { + const timeout = setTimeout(() => { + clearTimeout(timeout); + deferred.reject(new Error('Environment not detected')); + }, TEST_TIMEOUT); + await deferred.promise; + } + + async function isLocated(executable: string): Promise { + 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) { + 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(); + 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(); + 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(); + // 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 { + 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 { + 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(); + 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(); + 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 { + 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 { + 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/ + + spam.txt + sub2.2/ + + python3.exe + sub3/ + python.exe + + spam.txt + + eggs.exe + python2.exe + + `); + } else { + await ensureFSTree(` + ./.data/ + ${rootName}/ + sub1/ + spam + sub2/ + sub2.1/ + sub2.1.1/ + + spam.txt + sub2.2/ + + python3 + sub3/ + python + + spam.txt + + eggs + python2 + + 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}/ + + + + + + + # should match but doesn't + # should match but doesn't + # should match but doesn't + # should match but doesn't + # should match but doesn't + # should match but doesn't + # should match but doesn't + # should match but doesn't + + + # should match but doesn't + `); + } else { + await ensureFSTree(` + ./.data/ + ${rootName}/ + + + + + + + # should match but doesn't + # should match but doesn't + # should match but doesn't + # should match but doesn't + # should match but doesn't + # should match but doesn't + # should match but doesn't + # should match but doesn't + + + # 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/ + + + + 3.8/ + bin/ + + + + python/ + bin/ + + + + 3.8/ + bin/ + + + + python2/ + + python3/ + + python3.8/ + bin/ + + + python38/ + bin/ + + python.3.8/ + bin/ + + + python-3.8/ + bin/ + + + my-python/ + + 3.8/ + bin/ + + + + `); + } else { + await ensureFSTree(` + ./.data/ + ${rootName}/ + py/ + 2.7/ + bin/ + + + + 3.8/ + bin/ + + + + python/ + bin/ + + + + 3.8/ + bin/ + + + + python2/ + + python3/ + + python3.8/ + bin/ + + + python38/ + bin/ + + python.3.8/ + bin/ + + + python-3.8/ + bin/ + + + my-python/ + + 3.8/ + bin/ + + + + `); + } + }); + + 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/ + + + + + + + # should match but doesn't + # should match but doesn't + python2.7.exe/ + + python3.8.exe/ + + # launcher not supported + # launcher not supported + # case-insensitive + # case-insensitive + + + + + + + + + `); + } else { + await ensureFSTree(` + ./.data/ + ${rootName}/ + pythons/ + + + + + + + # should match but doesn't + # should match but doesn't + # launcher not supported + # launcher not supported + + + + + + + + + `); + } + }); + + 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 => { + 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 => { + 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, verify: VerifyOptions = {}) { + return async (file: string, args: string[], options: ShellOptions): Promise> => { + 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 /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> => { + /// 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>({ 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>({ 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>({ 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>({ 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>({ 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>({ 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(); + 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/src/test/pythonFiles/environments/conda/bin/python b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/_runtime_store/completed similarity index 100% rename from src/test/pythonFiles/environments/conda/bin/python rename to src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/_runtime_store/completed diff --git a/src/test/pythonFiles/environments/conda/envs/numpy/bin/python b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3 similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/numpy/bin/python rename to src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3 diff --git a/src/test/pythonFiles/environments/conda/envs/numpy/python.exe b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3.exe similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/numpy/python.exe 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/src/test/pythonFiles/environments/conda/envs/scipy/bin/python b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/_runtime_store/completed similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/scipy/bin/python rename to src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/_runtime_store/completed diff --git a/src/test/pythonFiles/environments/conda/envs/scipy/python.exe b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3 similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/scipy/python.exe rename to src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3 diff --git a/src/test/pythonFiles/environments/path1/one b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3.exe similarity index 100% rename from src/test/pythonFiles/environments/path1/one 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/src/test/pythonFiles/environments/path1/one.exe b/src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/condaLackingPython/bin/dummy similarity index 100% rename from src/test/pythonFiles/environments/path1/one.exe 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/path1/python.exe b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/bin/python similarity index 100% rename from src/test/pythonFiles/environments/path1/python.exe 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/path2/one b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/bin/python similarity index 100% rename from src/test/pythonFiles/environments/path2/one 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/path2/one.exe b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/bin/python similarity index 100% rename from src/test/pythonFiles/environments/path2/one.exe 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/src/test/pythonFiles/environments/path2/python.exe b/src/test/pythonEnvironments/common/envlayouts/hatch/project1/.gitkeep similarity index 100% rename from src/test/pythonFiles/environments/path2/python.exe 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/test/pythonFiles/folding/empty.py b/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/parent/child/folder/dummyFile similarity index 100% rename from src/test/pythonFiles/folding/empty.py 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/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py310/conda-meta/pixi b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py310/conda-meta/pixi new file mode 100644 index 000000000000..e69de29bb2d1 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/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/conda-meta/pixi b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/conda-meta/pixi new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/python b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/python new file mode 100644 index 000000000000..e69de29bb2d1 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/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/bin/python b/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/conda-meta/pixi b/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/conda-meta/pixi new file mode 100644 index 000000000000..e69de29bb2d1 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 "] +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 "] +channels = ["conda-forge"] +platforms = ["win-64"] + +[tasks] + +[dependencies] +python = "~=3.8.0" diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/Scripts/activate b/src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/Scripts/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/Scripts/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 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/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/Scripts/activate b/src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/Scripts/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/Scripts/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 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/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/activate b/src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/python b/src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/bin/activate b/src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/bin/python b/src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/project1-haha-py3.8/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/project1-haha-py3.8/Scripts/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 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 "] + +[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/project2/.venv/bin/activate b/src/test/pythonEnvironments/common/envlayouts/poetry/project2/.venv/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/project2/.venv/bin/python b/src/test/pythonEnvironments/common/envlayouts/poetry/project2/.venv/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 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 "] + +[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 { + const someFilePath = 'requirements.txt'; + const someFile = typemoq.Mock.ofType(); + 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; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock; + + setup(() => { + configMock = typemoq.Mock.ofType(); + plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); + interpreterService = typemoq.Mock.ofType(); + 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('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('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('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; + let interpreterPathService: typemoq.IMock; + let pathUtils: typemoq.IMock; + + setup(() => { + showQuickPickStub = sinon.stub(windowApis, 'showQuickPick'); + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + + registerCommandStub = sinon.stub(commandApis, 'registerCommand'); + interpreterQuickPick = typemoq.Mock.ofType(); + interpreterPathService = typemoq.Mock.ofType(); + pathUtils = typemoq.Mock.ofType(); + + 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(); + 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; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + onDidChangeConfigurationStub = sinon.stub(workspaceApis, 'onDidChangeConfiguration'); + onDidChangeConfigurationStub.returns(new FakeDisposable()); + + configMock = typemoq.Mock.ofType(); + configMock.setup((c) => c.get(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(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(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(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(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(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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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); + let execEvent: typemoq.IMock; + let shellIntegration: typemoq.IMock; + + 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(); + execEvent = typemoq.Mock.ofType(); + 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 { + const someFilePath = 'something.py'; + const someFile = typemoq.Mock.ofType(); + 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 { + const someFilePath = 'requirements.txt'; + const someFile = typemoq.Mock.ofType(); + 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 { + const someFilePath = 'pyproject.toml'; + const someFile = typemoq.Mock.ofType(); + 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 { + const someFilePath = 'something.toml'; + const someFile = typemoq.Mock.ofType(); + 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; + let getActiveTextEditorStub: sinon.SinonStub; + let textEditor: typemoq.IMock; + let getInstalledPackagesDiagnosticsStub: sinon.SinonStub; + let interpreterService: typemoq.IMock; + + 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.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(); + getActiveTextEditorStub.returns(textEditor.object); + + getInstalledPackagesDiagnosticsStub = sinon.stub(installUtils, 'getInstalledPackagesDiagnostics'); + interpreterService = typemoq.Mock.ofType(); + 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; + 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(); + 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) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => 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, + ) => 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) => 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, + ) => 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) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 1, + }, + out: { + subscribe: ( + next?: (value: Output) => 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, + ) => 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; + let progressMock: typemoq.IMock; + 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(); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + pickPackagesToInstallStub = sinon.stub(venvUtils, 'pickPackagesToInstall'); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + + progressMock = typemoq.Mock.ofType(); + 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) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => 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, + ) => 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) => 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, + ) => 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) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 1, + }, + out: { + subscribe: ( + next?: (value: Output) => 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, + ) => 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) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output) => 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, + ) => 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) => 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) => 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, + ) => 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) => 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) => 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, + ) => 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; + + setup(() => { + sendTelemetryEventStub = sinon.stub(telemetry, 'sendTelemetryEvent'); + progressReporterMock = typemoq.Mock.ofType(); + }); + + 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 { + const pyprojectTomlPath = 'pyproject.toml'; + const pyprojectToml = typemoq.Mock.ofType(); + 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 { + const pyprojectTomlPath = 'pyproject.toml'; + const pyprojectToml = typemoq.Mock.ofType(); + 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 { + const someFilePath = 'something.py'; + const someFile = typemoq.Mock.ofType(); + 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/globalenv.unit.test.ts b/src/test/pythonEnvironments/discovery/globalenv.unit.test.ts new file mode 100644 index 000000000000..f8240b996f7c --- /dev/null +++ b/src/test/pythonEnvironments/discovery/globalenv.unit.test.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +suite('getPyenvTypeFinder()', () => { + // We will pull tests over from src/test/interpreters/virtualEnvs/index.unit.test.ts at some point. +}); + +suite('getPyenvRootFinder()', () => { + // We will pull tests over from src/test/interpreters/virtualEnvs/index.unit.test.ts at some point. +}); diff --git a/src/test/pythonEnvironments/discovery/locators/condaService.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/condaService.unit.test.ts new file mode 100644 index 000000000000..95e94cfc4584 --- /dev/null +++ b/src/test/pythonEnvironments/discovery/locators/condaService.unit.test.ts @@ -0,0 +1,114 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import { FileSystemPaths, FileSystemPathUtils } from '../../../../client/common/platform/fs-paths'; +import { IFileSystem, IPlatformService } from '../../../../client/common/platform/types'; +import { CondaService } from '../../../../client/pythonEnvironments/common/environmentManagers/condaService'; +import { Conda } from '../../../../client/pythonEnvironments/common/environmentManagers/conda'; + +suite('Interpreters Conda Service', () => { + let platformService: TypeMoq.IMock; + let condaService: CondaService; + let fileSystem: TypeMoq.IMock; + setup(async () => { + platformService = TypeMoq.Mock.ofType(); + fileSystem = TypeMoq.Mock.ofType(); + + fileSystem + .setup((fs) => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((p1, p2) => { + const utils = FileSystemPathUtils.withDefaults( + FileSystemPaths.withDefaults(platformService.object.isWindows), + ); + return utils.arePathsSame(p1, p2); + }); + + condaService = new CondaService(platformService.object, fileSystem.object); + sinon.stub(Conda, 'getConda').callsFake(() => Promise.resolve(undefined)); + }); + teardown(() => sinon.restore()); + + type InterpreterSearchTestParams = { + pythonPath: string; + environmentName: string; + isLinux: boolean; + expectedCondaPath: string; + }; + + const testsForInterpreter: InterpreterSearchTestParams[] = [ + { + pythonPath: path.join('users', 'foo', 'envs', 'test1', 'python'), + environmentName: 'test1', + isLinux: true, + expectedCondaPath: path.join('users', 'foo', 'bin', 'conda'), + }, + { + pythonPath: path.join('users', 'foo', 'envs', 'test2', 'python'), + environmentName: 'test2', + isLinux: true, + expectedCondaPath: path.join('users', 'foo', 'envs', 'test2', 'conda'), + }, + { + pythonPath: path.join('users', 'foo', 'envs', 'test3', 'python'), + environmentName: 'test3', + isLinux: false, + expectedCondaPath: path.join('users', 'foo', 'Scripts', 'conda.exe'), + }, + { + pythonPath: path.join('users', 'foo', 'envs', 'test4', 'python'), + environmentName: 'test4', + isLinux: false, + expectedCondaPath: path.join('users', 'foo', 'conda.exe'), + }, + ]; + + testsForInterpreter.forEach((t) => { + test(`Finds conda.exe for subenvironment ${t.environmentName}`, async () => { + platformService.setup((p) => p.isLinux).returns(() => t.isLinux); + platformService.setup((p) => p.isWindows).returns(() => !t.isLinux); + platformService.setup((p) => p.isMac).returns(() => false); + fileSystem + .setup((f) => + f.fileExists( + TypeMoq.It.is((p) => { + if (p === t.expectedCondaPath) { + return true; + } + return false; + }), + ), + ) + .returns(() => Promise.resolve(true)); + + const condaFile = await condaService.getCondaFileFromInterpreter(t.pythonPath, t.environmentName); + assert.strictEqual(condaFile, t.expectedCondaPath); + }); + test(`Finds conda.exe for different ${t.environmentName}`, async () => { + platformService.setup((p) => p.isLinux).returns(() => t.isLinux); + platformService.setup((p) => p.isWindows).returns(() => !t.isLinux); + platformService.setup((p) => p.isMac).returns(() => false); + fileSystem + .setup((f) => + f.fileExists( + TypeMoq.It.is((p) => { + if (p === t.expectedCondaPath) { + return true; + } + return false; + }), + ), + ) + .returns(() => Promise.resolve(true)); + + const condaFile = await condaService.getCondaFileFromInterpreter(t.pythonPath, undefined); + + // This should only work if the expectedConda path has the original environment name in it + if (t.expectedCondaPath.includes(t.environmentName)) { + assert.strictEqual(condaFile, t.expectedCondaPath); + } else { + assert.strictEqual(condaFile, 'conda'); + } + }); + }); +}); 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) + + + + + + # should match but doesn't + # + # + # should match but doesn't + python.txt + # should match but doesn't + + spam.txt + parent/ + root2/ + + + 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 + #python2.exe # matches on Windows (not actually executable though) + root6/ # no matching executables + + spam.txt + + + `.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/info/executable.unit.test.ts b/src/test/pythonEnvironments/info/executable.unit.test.ts new file mode 100644 index 000000000000..bb6ecd7acabc --- /dev/null +++ b/src/test/pythonEnvironments/info/executable.unit.test.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +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'; + +interface IDeps { + shellExec(command: string, options: ShellOptions | undefined): Promise>; +} + +suite('getExecutablePath()', () => { + let deps: IMock; + const python = buildPythonExecInfo('path/to/python'); + + setup(() => { + deps = Mock.ofType(undefined, MockBehavior.Strict); + }); + + test('should get the value by running python', async () => { + const expected = 'path/to/dummy/executable'; + 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: ShellOptions | undefined) => deps.object.shellExec(c, a); + + const result = await getExecutablePath(python, exec); + + expect(result).to.equal(expected, 'getExecutablePath() should return get the value by running Python'); + deps.verifyAll(); + }); + + test('should throw if exec() fails', async () => { + const stderr = 'oops'; + 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: ShellOptions | undefined) => deps.object.shellExec(c, a); + + const promise = getExecutablePath(python, exec); + + 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 new file mode 100644 index 000000000000..967454dd6c7e --- /dev/null +++ b/src/test/pythonEnvironments/info/interpreter.unit.test.ts @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { join as pathJoin } from 'path'; +import { SemVer } from 'semver'; +import { IMock, It, 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 script = pathJoin(EXTENSION_ROOT_DIR_FOR_TESTS, 'python_files', 'interpreterInfo.py'); + +suite('extractInterpreterInfo()', () => { + // Tests go here. +}); + +type ShellExecResult = { + stdout: string; + stderr?: string; +}; +interface IDeps { + shellExec(command: string, options?: ShellOptions | undefined): Promise; +} + +suite('getInterpreterInfo()', () => { + let deps: IMock; + const python = buildPythonExecInfo('path/to/python'); + + setup(() => { + deps = Mock.ofType(undefined, MockBehavior.Strict); + }); + + test('should call exec() with the proper command and timeout', async () => { + const json = { + versionInfo: [3, 7, 5, 'candidate', 1], + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true, + }; + const cmd = `"${python.command}" "${script}"`; + deps + // Checking the args is the key point of this test. + .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); + + deps.verifyAll(); + }); + + test('should quote spaces in the command', async () => { + const json = { + versionInfo: [3, 7, 5, 'candidate', 1], + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true, + }; + const _python = buildPythonExecInfo(' path to /my python '); + const cmd = `" path to /my python " "${script}"`; + deps + // Checking the args is the key point of this test. + .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); + + deps.verifyAll(); + }); + + test('should handle multi-command (e.g. conda)', async () => { + const json = { + versionInfo: [3, 7, 5, 'candidate', 1], + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true, + }; + const _python = buildPythonExecInfo(['path/to/conda', 'run', '-n', 'my-env', 'python']); + const cmd = `"path/to/conda" "run" "-n" "my-env" "python" "${script}"`; + deps + // Checking the args is the key point of this test. + .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); + + deps.verifyAll(); + }); + + test('should return an object if exec() is successful', async () => { + const expected = { + architecture: Architecture.x64, + path: python.command, + version: new SemVer('3.7.5-candidate1'), + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + sysVersion: undefined, + }; + const json = { + versionInfo: [3, 7, 5, 'candidate', 1], + sysPrefix: expected.sysPrefix, + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true, + }; + deps + // We check the args in other tests. + .setup((d) => d.shellExec(TypeMoqIt.isAny(), TypeMoqIt.isAny())) + .returns(() => + Promise.resolve({ + stdout: JSON.stringify(json), + }), + ); + const shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); + + const result = await getInterpreterInfo(python, shellExec); + + expect(result).to.deep.equal(expected, 'broken'); + deps.verifyAll(); + }); + + test('should return an object if the version info contains less than 4 items', async () => { + const expected = { + architecture: Architecture.x64, + path: python.command, + version: new SemVer('3.7.5'), + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + sysVersion: undefined, + }; + const json = { + versionInfo: [3, 7, 5], + sysPrefix: expected.sysPrefix, + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true, + }; + deps + // We check the args in other tests. + .setup((d) => d.shellExec(TypeMoqIt.isAny(), TypeMoqIt.isAny())) + .returns(() => + Promise.resolve({ + stdout: JSON.stringify(json), + }), + ); + const shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); + + const result = await getInterpreterInfo(python, shellExec); + + expect(result).to.deep.equal(expected, 'broken'); + deps.verifyAll(); + }); + + test('should return an object with the architecture value set to x86 if json.is64bit is not 64bit', async () => { + const expected = { + architecture: Architecture.x86, + path: python.command, + version: new SemVer('3.7.5-candidate'), + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + sysVersion: undefined, + }; + const json = { + versionInfo: [3, 7, 5, 'candidate'], + sysPrefix: expected.sysPrefix, + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: false, + }; + deps + // We check the args in other tests. + .setup((d) => d.shellExec(TypeMoqIt.isAny(), TypeMoqIt.isAny())) + .returns(() => + Promise.resolve({ + stdout: JSON.stringify(json), + }), + ); + const shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); + + const result = await getInterpreterInfo(python, shellExec); + + expect(result).to.deep.equal(expected, 'broken'); + deps.verifyAll(); + }); + + test('should return undefined if the result of exec() writes to stderr', async () => { + const err = new StdErrError('oops!'); + deps + // We check the args in other tests. + .setup((d) => d.shellExec(TypeMoqIt.isAny(), TypeMoqIt.isAny())) + .returns(() => Promise.reject(err)); + const shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); + + const result = getInterpreterInfo(python, shellExec); + + await expect(result).to.eventually.be.rejectedWith(err); + deps.verifyAll(); + }); + + test('should fail if exec() fails (e.g. the script times out)', async () => { + const err = new Error('oops'); + deps + // We check the args in other tests. + .setup((d) => d.shellExec(TypeMoqIt.isAny(), TypeMoqIt.isAny())) + .returns(() => Promise.reject(err)); + const shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); + + const result = getInterpreterInfo(python, shellExec); + + await expect(result).to.eventually.be.rejectedWith(err); + deps.verifyAll(); + }); + + test('should fail if the json value returned by interpreterInfo.py is not valid', async () => { + deps + // We check the args in other tests. + .setup((d) => d.shellExec(TypeMoqIt.isAny(), TypeMoqIt.isAny())) + .returns(() => Promise.resolve({ stdout: 'bad json' })); + const shellExec = async (c: string, t: ShellOptions | undefined) => deps.object.shellExec(c, t); + + const result = getInterpreterInfo(python, shellExec); + + await expect(result).to.eventually.be.rejected; + deps.verifyAll(); + }); +}); diff --git a/src/test/pythonEnvironments/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 { + initializeExternalDependencies(serviceContainer); + // The old tests do not need real instances, directly pass in mocks. + registerNewDiscoveryForIOC(serviceManager, instance(mock())); +} 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; + let setCondaBinaryStub: sinon.SinonStub; + let getCondaPathSettingStub: sinon.SinonStub; + let getCondaEnvDirsStub: sinon.SinonStub; + let setPyEnvBinaryStub: sinon.SinonStub; + let createPythonWatcherStub: sinon.SinonStub; + let mockWatcher: typemoq.IMock; + 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(); + 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(); + 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; + 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(); + configMock.setup((c) => c.get('venvPath')).returns(() => undefined); + configMock.setup((c) => c.get('venvFolders')).returns(() => []); + configMock.setup((c) => c.get('condaPath')).returns(() => ''); + configMock.setup((c) => c.get('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 = "" % 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 "" % (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<= 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 c67dc9f106a6..000000000000 --- a/src/test/pythonFiles/folding/attach_server.py +++ /dev/null @@ -1,330 +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" - -__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') - -_attach_enabled = False -_attached = threading.Event() -vspd.DONT_DEBUG.append(os.path.normcase(__file__)) - - -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)) - 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 ca4d3f4140a6..000000000000 --- a/src/test/pythonFiles/folding/noComments.py +++ /dev/null @@ -1,278 +0,0 @@ -__author__ = "Microsoft Corporation " -__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') - -_attach_enabled = False -_attached = threading.Event() -vspd.DONT_DEBUG.append(os.path.normcase(__file__)) - - -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)) - 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 9fd4b4874a57..000000000000 --- a/src/test/pythonFiles/folding/noDocStrings.py +++ /dev/null @@ -1,266 +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" - -__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') - -_attach_enabled = False -_attached = threading.Event() -vspd.DONT_DEBUG.append(os.path.normcase(__file__)) - - -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)) - 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 " -__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 " -__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 " -__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('')) -if sys.version_info >= (3, 5): - DONT_DEBUG.append(path.normcase('')) - -# Contains information about all breakpoints in the process. Keys are line numbers on which -# there are breakpoints in any file, and values are dicts. For every line number, the -# corresponding dict contains all the breakpoints that fall on that line. The keys in that -# dict are tuples of the form (filename, breakpoint_id), each entry representing a single -# breakpoint, and values are BreakpointInfo objects. -# -# For example, given the following breakpoints: -# -# 1. In 'main.py' at line 10. -# 2. In 'main.py' at line 20. -# 3. In 'module.py' at line 10. -# -# the contents of BREAKPOINTS would be: -# {10: {('main.py', 1): ..., ('module.py', 3): ...}, 20: {('main.py', 2): ... }} -BREAKPOINTS = {} - -# Contains information about all pending (i.e. not yet bound) breakpoints in the process. -# Elements are BreakpointInfo objects. -PENDING_BREAKPOINTS = set() - -# Must be in sync with enum PythonBreakpointConditionKind in PythonBreakpoint.cs -BREAKPOINT_CONDITION_ALWAYS = 0 -BREAKPOINT_CONDITION_WHEN_TRUE = 1 -BREAKPOINT_CONDITION_WHEN_CHANGED = 2 - -# Must be in sync with enum PythonBreakpointPassCountKind in PythonBreakpoint.cs -BREAKPOINT_PASS_COUNT_ALWAYS = 0 -BREAKPOINT_PASS_COUNT_EVERY = 1 -BREAKPOINT_PASS_COUNT_WHEN_EQUAL = 2 -BREAKPOINT_PASS_COUNT_WHEN_EQUAL_OR_GREATER = 3 - -## 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(''): ((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 14259db2e30e..000000000000 --- a/src/test/pythonFiles/folding/visualstudio_py_repl.py +++ /dev/null @@ -1,513 +0,0 @@ -# Python Tools for Visual Studio - -# Copyright(c) Microsoft Corporation - -# All rights reserved. - -from __future__ import with_statement - -__author__ = "Microsoft Corporation " -__version__ = "3.0.0.0" - -# This module MUST NOT import threading in global scope. This is because in a direct (non-ptvsd) - -# attach scenario, it is loaded on the injected debugger attach thread, and if threading module - -# hasn't been loaded already, it will assume that the thread on which it is being loaded is the - -# main thread. This will cause issues when the thread goes away after attach completes. - -try: - import thread -except ImportError: - # Renamed in Python3k - import _thread as thread -try: - from ssl import SSLError -except: - SSLError = None - -import sys -import socket -import select -import time -import struct -import imp -import traceback -import random -import os -import inspect -import types -from collections import deque - -try: - # In the local attach scenario, visualstudio_py_util is injected into globals() - - # by PyDebugAttach before loading this module, and cannot be imported. - _vspu = visualstudio_py_util -except: - try: - import visualstudio_py_util as _vspu - except ImportError: - import ptvsd.visualstudio_py_util as _vspu -to_bytes = _vspu.to_bytes -read_bytes = _vspu.read_bytes -read_int = _vspu.read_int -read_string = _vspu.read_string -write_bytes = _vspu.write_bytes -write_int = _vspu.write_int -write_string = _vspu.write_string - -try: - unicode -except NameError: - unicode = str - -try: - BaseException -except NameError: - # BaseException not defined until Python 2.5 - BaseException = Exception - -DEBUG = os.environ.get('DEBUG_REPL') is not None - -__all__ = ['ReplBackend', 'BasicReplBackend', 'BACKEND'] - -def _debug_write(out): - if DEBUG: - sys.__stdout__.write(out) - sys.__stdout__.flush() - - -class SafeSendLock(object): - """a lock which ensures we're released if we take a KeyboardInterrupt exception acquiring it""" - def __init__(self): - self.lock = thread.allocate_lock() - - def __enter__(self): - self.acquire() - - def __exit__(self, exc_type, exc_value, tb): - self.release() - - def acquire(self): - try: - self.lock.acquire() - except KeyboardInterrupt: - try: - self.lock.release() - except: - pass - raise - - def release(self): - self.lock.release() - -def _command_line_to_args_list(cmdline): - """splits a string into a list using Windows command line syntax.""" - args_list = [] - - if cmdline and cmdline.strip(): - from ctypes import c_int, c_voidp, c_wchar_p - from ctypes import byref, POINTER, WinDLL - - clta = WinDLL('shell32').CommandLineToArgvW - clta.argtypes = [c_wchar_p, POINTER(c_int)] - clta.restype = POINTER(c_wchar_p) - - lf = WinDLL('kernel32').LocalFree - lf.argtypes = [c_voidp] - - pNumArgs = c_int() - r = clta(cmdline, byref(pNumArgs)) - if r: - for index in range(0, pNumArgs.value): - if sys.hexversion >= 0x030000F0: - argval = r[index] - else: - argval = r[index].encode('ascii', 'replace') - args_list.append(argval) - lf(r) - else: - sys.stderr.write('Error parsing script arguments:\n') - sys.stderr.write(cmdline + '\n') - - return args_list - - -class UnsupportedReplException(Exception): - def __init__(self, reason): - self.reason = reason - -# save the start_new_thread so we won't debug/break into the REPL comm thread. -start_new_thread = thread.start_new_thread -class ReplBackend(object): - """back end for executing REPL code. This base class handles all of the communication with the remote process while derived classes implement the actual inspection and introspection.""" - _MRES = to_bytes('MRES') - _SRES = to_bytes('SRES') - _MODS = to_bytes('MODS') - _IMGD = to_bytes('IMGD') - _PRPC = to_bytes('PRPC') - _RDLN = to_bytes('RDLN') - _STDO = to_bytes('STDO') - _STDE = to_bytes('STDE') - _DBGA = to_bytes('DBGA') - _DETC = to_bytes('DETC') - _DPNG = to_bytes('DPNG') - _DXAM = to_bytes('DXAM') - _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 occured on the socket... - raise - continue - - self.conn.settimeout(None) - if inp == '': - break - self.flush() - - cmd = ReplBackend._COMMANDS.get(inp) - if cmd is not None: - cmd(self) - except: - _debug_write('error in repl loop') - _debug_write(traceback.format_exc()) - self.exit_process() - - time.sleep(2) # try and exit gracefully, then interrupt main if necessary - - if sys.platform == 'cli': - # just kill us as fast as possible - import System - System.Environment.Exit(1) - - self.interrupt_main() - - def check_for_exit_repl_loop(self): - return False - - def _cmd_run(self): - """runs the received snippet of code""" - self.run_command(read_string(self.conn)) - - def _cmd_abrt(self): - """aborts the current running command""" - # abort command, interrupts execution of the main thread. - self.interrupt_main() - - def _cmd_exit(self): - """exits the interactive process""" - self.exit_requested = True - self.exit_process() - - def _cmd_mems(self): - """gets the list of members available for the given expression""" - expression = read_string(self.conn) - try: - name, inst_members, type_members = self.get_members(expression) - except: - with self.send_lock: - write_bytes(self.conn, ReplBackend._MERR) - _debug_write('error in eval') - _debug_write(traceback.format_exc()) - else: - with self.send_lock: - write_bytes(self.conn, ReplBackend._MRES) - write_string(self.conn, name) - self._write_member_dict(inst_members) - self._write_member_dict(type_members) - - def _cmd_sigs(self): - """gets the signatures for the given expression""" - expression = read_string(self.conn) - try: - sigs = self.get_signatures(expression) - except: - with self.send_lock: - write_bytes(self.conn, ReplBackend._SERR) - _debug_write('error in eval') - _debug_write(traceback.format_exc()) - else: - with self.send_lock: - write_bytes(self.conn, ReplBackend._SRES) - # single overload - write_int(self.conn, len(sigs)) - for doc, args, vargs, varkw, defaults in sigs: - # write overload - write_string(self.conn, (doc or '')[:4096]) - arg_count = len(args) + (vargs is not None) + (varkw is not None) - write_int(self.conn, arg_count) - - def_values = [''] * (len(args) - len(defaults)) + ['=' + d for d in defaults] - for arg, def_value in zip(args, def_values): - write_string(self.conn, (arg or '') + def_value) - if vargs is not None: - write_string(self.conn, '*' + vargs) - if varkw is not None: - write_string(self.conn, '**' + varkw) - - def _cmd_setm(self): - global exec_mod - """sets the current module which code will execute against""" - mod_name = read_string(self.conn) - self.set_current_module(mod_name) - - def _cmd_sett(self): - """sets the current thread and frame which code will execute against""" - thread_id = read_int(self.conn) - frame_id = read_int(self.conn) - frame_kind = read_int(self.conn) - self.set_current_thread_and_frame(thread_id, frame_id, frame_kind) - - def _cmd_mods(self): - """gets the list of available modules""" - try: - res = self.get_module_names() - res.sort() - except: - res = [] - - with self.send_lock: - write_bytes(self.conn, ReplBackend._MODS) - write_int(self.conn, len(res)) - for name, filename in res: - write_string(self.conn, name) - write_string(self.conn, filename) - - def _cmd_inpl(self): - """handles the input command which returns a string of input""" - self.input_string = read_string(self.conn) - self.input_event.release() - - def _cmd_excf(self): - """handles executing a single file""" - filename = read_string(self.conn) - args = read_string(self.conn) - self.execute_file(filename, args) - - def _cmd_excx(self): - """handles executing a single file, module or process""" - filetype = read_string(self.conn) - filename = read_string(self.conn) - args = read_string(self.conn) - self.execute_file_ex(filetype, filename, args) - - def _cmd_debug_attach(self): - import visualstudio_py_debugger - port = read_int(self.conn) - id = read_string(self.conn) - debug_options = visualstudio_py_debugger.parse_debug_options(read_string(self.conn)) - self.attach_process(port, id, debug_options) - - _COMMANDS = { - to_bytes('run '): _cmd_run, - to_bytes('abrt'): _cmd_abrt, - to_bytes('exit'): _cmd_exit, - to_bytes('mems'): _cmd_mems, - to_bytes('sigs'): _cmd_sigs, - to_bytes('mods'): _cmd_mods, - to_bytes('setm'): _cmd_setm, - to_bytes('sett'): _cmd_sett, - to_bytes('inpl'): _cmd_inpl, - to_bytes('excf'): _cmd_excf, - to_bytes('excx'): _cmd_excx, - to_bytes('dbga'): _cmd_debug_attach, - } - - def _write_member_dict(self, mem_dict): - write_int(self.conn, len(mem_dict)) - for name, type_name in mem_dict.items(): - write_string(self.conn, name) - write_string(self.conn, type_name) - - def on_debugger_detach(self): - with self.send_lock: - write_bytes(self.conn, ReplBackend._DETC) - - def init_debugger(self): - from os import path - sys.path.append(path.dirname(__file__)) - import visualstudio_py_debugger - visualstudio_py_debugger.DONT_DEBUG.append(path.normcase(__file__)) - new_thread = visualstudio_py_debugger.new_thread() - sys.settrace(new_thread.trace_func) - visualstudio_py_debugger.intercept_threads(True) - - def send_image(self, filename): - with self.send_lock: - write_bytes(self.conn, ReplBackend._IMGD) - write_string(self.conn, filename) - - def write_png(self, image_bytes): - with self.send_lock: - write_bytes(self.conn, ReplBackend._DPNG) - write_int(self.conn, len(image_bytes)) - write_bytes(self.conn, image_bytes) - - def write_xaml(self, xaml_bytes): - with self.send_lock: - write_bytes(self.conn, ReplBackend._DXAM) - write_int(self.conn, len(xaml_bytes)) - write_bytes(self.conn, xaml_bytes) - - def send_prompt(self, ps1, ps2, 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/autopep8.output b/src/test/pythonFiles/formatting/autopep8.output deleted file mode 100644 index 9050345d0575..000000000000 --- a/src/test/pythonFiles/formatting/autopep8.output +++ /dev/null @@ -1,49 +0,0 @@ ---- original//Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/formatting/autoPep8FileToFormat.py -+++ fixed//Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/formatting/autoPep8FileToFormat.py -@@ -1,21 +1,31 @@ --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) diff --git a/src/test/pythonFiles/formatting/black.output b/src/test/pythonFiles/formatting/black.output deleted file mode 100644 index be709f2d720a..000000000000 --- a/src/test/pythonFiles/formatting/black.output +++ /dev/null @@ -1,54 +0,0 @@ ---- src/test/pythonFiles/formatting/fileToFormat.py (original) -+++ src/test/pythonFiles/formatting/fileToFormat.py (formatted) -@@ -1,22 +1,38 @@ --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/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 b04a9a16ffaa..000000000000 --- a/src/test/pythonFiles/formatting/fileToFormat.py +++ /dev/null @@ -1,21 +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, "", "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 0e2ce688a3d6..000000000000 --- a/src/test/pythonFiles/formatting/yapf.output +++ /dev/null @@ -1,59 +0,0 @@ ---- /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/formatting/yapfFileToFormat.py (original) -+++ /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/formatting/yapfFileToFormat.py (reformatted) -@@ -1,21 +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) 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/pep8config/.pep8 b/src/test/pythonFiles/linting/pep8config/.pep8 deleted file mode 100644 index 40c4dff4d14b..000000000000 --- a/src/test/pythonFiles/linting/pep8config/.pep8 +++ /dev/null @@ -1,2 +0,0 @@ -[pep8] -ignore = E302,E901,E127,E261,E261,E261,E303 \ No newline at end of file diff --git a/src/test/pythonFiles/linting/pep8config/file.py b/src/test/pythonFiles/linting/pep8config/file.py deleted file mode 100644 index 047ba0dc679e..000000000000 --- a/src/test/pythonFiles/linting/pep8config/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/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/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 | - +-----------------+ - | | - +----+------------+ - | | AIFC | - | +------------+ - | | | - | | . | - | | . | - | | . | - +----+------------+ - -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 - (AIFF-C only). - MARK - <# of markers> (2 bytes) - list of markers: - (2 bytes, must be > 0) - (4 bytes) - ("pstring") - COMM - <# of channels> (2 bytes) - <# of sound frames> (4 bytes) - (2 bytes) - (10 bytes, IEEE 80-bit extended - floating point) - in AIFF-C files only: - (4 bytes) - ("pstring") - SSND - (4 bytes, not used by this program) - (4 bytes, not used by this program) - - -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 ` - - -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 be825150a841..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 variale - """ - 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.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='' mode='w' encoding='UTF-8'>, loggingConfig=None, options=, parser=, parserClass=, plugins=, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='' 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.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.plugins.collect: DEBUG: Add test -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -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.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='' mode='w' encoding='UTF-8'>, loggingConfig=None, options=, parser=, parserClass=, plugins=, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='' 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.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.plugins.collect: DEBUG: Add test -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test()]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test(), Test()]> -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.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='' mode='w' encoding='UTF-8'>, loggingConfig=None, options=, parser=, parserClass=, plugins=, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='' 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.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.plugins.collect: DEBUG: Add test -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -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() -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test(), Test()]>]> -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.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='' mode='w' encoding='UTF-8'>, loggingConfig=None, options=, parser=, parserClass=, plugins=, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='' 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.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.plugins.collect: DEBUG: Add test -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -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() -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test(), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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 ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test(), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test(), Test(), Test()]>, ), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]>]> -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 @@ - 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.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='' mode='w' encoding='UTF-8'>, loggingConfig=None, options=, parser=, parserClass=, plugins=, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='' 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.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.plugins.collect: DEBUG: Add test -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -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() -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test(), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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 ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test(), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test(), Test(), Test()]>, ), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]>]> -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 @@ - 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.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='' mode='w' encoding='UTF-8'>, loggingConfig=None, options=, parser=, parserClass=, plugins=, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='' 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.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.plugins.collect: DEBUG: Add test -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -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() -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test(), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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 ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test(), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test(), Test(), Test()]>, ), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]>]> -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 @@ - 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.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='' mode='w' encoding='UTF-8'>, loggingConfig=None, options=, parser=, parserClass=, plugins=, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='' 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.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.plugins.collect: DEBUG: Add test -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -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() -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test(), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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 ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test(), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test(), Test(), Test()]>, ), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]>]> -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 @@ - 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 @@ - 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.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='' mode='w' encoding='UTF-8'>, loggingConfig=None, options=, parser=, parserClass=, plugins=, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='' 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.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.plugins.collect: DEBUG: Add test -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -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() -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test(), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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 ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test(), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test(), Test(), Test()]>, ), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]>]> -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 @@ - 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.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='' mode='w' encoding='UTF-8'>, loggingConfig=None, options=, parser=, parserClass=, plugins=, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='' 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.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.plugins.collect: DEBUG: Add test -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -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() -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test(), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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 ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test(), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test(), Test(), Test()]>, ), Test()]>]> -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test()]>]> -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.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='' mode='w' encoding='UTF-8'>, loggingConfig=None, options=, parser=, parserClass=, plugins=, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='' 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.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.plugins.collect: DEBUG: Add test -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -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() -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 -nose.selector: DEBUG: wantModule ? True -nose.selector: DEBUG: wantClass ? True -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod ? None -nose.selector: DEBUG: wantMethod >? None -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.selector: DEBUG: wantMethod ? True -nose.plugins.collect: DEBUG: TestSuite() -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.plugins.collect: DEBUG: Add test ), Test(), Test()]> -nose.plugins.collect: DEBUG: Add test ), Test(), Test()]>]> -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 e03b01dab403..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/five.output +++ /dev/null @@ -1,53 +0,0 @@ -============================= test session starts ============================== -platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 -rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard, inifile: -plugins: pylama-7.4.3 -collected 29 items - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -========================= no tests ran in 0.07 seconds ========================= 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 @@ -self = <test_root.Test_Root_test1 testMethod=test_Root_A> - - def test_Root_A(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -test_root.py:8: AssertionError 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 c7d46fd0124e..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/four.output +++ /dev/null @@ -1,53 +0,0 @@ -============================= test session starts ============================== -platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 -rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard, inifile: -plugins: pylama-7.4.3 -collected 29 items - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -========================= no tests ran in 0.05 seconds ========================= 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 @@ -self = <test_root.Test_Root_test1 testMethod=test_Root_A> - - def test_Root_A(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -test_root.py:8: AssertionErrortest_root.py:12: <py._xmlgen.raw object at 0x10a139048> 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 e2d04ab76d68..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/one.output +++ /dev/null @@ -1,64 +0,0 @@ -============================= test session starts ============================== -platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 -rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard, inifile: -plugins: pylama-7.4.3 -collected 33 items - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -========================= no tests ran in 0.06 seconds ========================= 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 @@ -self = <test_root.Test_Root_test1 testMethod=test_Root_A> - - def test_Root_A(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -test_root.py:8: AssertionErrortest_root.py:12: <py._xmlgen.raw object at 0x1024cf048>non_parametrized_username = 'three' - - def test_parametrized_username(non_parametrized_username): -> assert non_parametrized_username in ['one', 'two', 'threes'] -E AssertionError: assert 'three' in ['one', 'two', 'threes'] - -tests/test_another_pytest.py:17: AssertionErrorself = <tests.external.ForeignTests.TestExtraNestedForeignTests object at 0x10fb685c0> - - def test_super_deep_foreign(self): -> assert False -E AssertionError - -tests/external.py:4: AssertionErrorself = <tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere object at 0x10fb74898> - - def test_foreign_test(self): -> assert False -E AssertionError - -tests/external.py:6: AssertionError/Users/donjayamanne/anaconda3/lib/python3.6/site-packages/_pytest/nose.py:23: <py._xmlgen.raw object at 0x1024fb518>non_parametrized_username = 'three' - - def test_parametrized_username(non_parametrized_username): -> assert non_parametrized_username in ['one', 'two', 'threes'] -E AssertionError: assert 'three' in ['one', 'two', 'threes'] - -tests/test_pytest.py:40: AssertionErrorself = <test_unittest_one.Test_test1 testMethod=test_A> - - def test_A(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -tests/test_unittest_one.py:8: AssertionErrortests/test_unittest_one.py:12: <py._xmlgen.raw object at 0x102504cc0>self = <test_unittest_two.Test_test2 testMethod=test_A2> - - def test_A2(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -tests/test_unittest_two.py:5: AssertionErrorself = <test_unittest_two.Test_test2 testMethod=test_C2> - - def test_C2(self): -> self.assertEqual(1,2,'Not equal') -E AssertionError: 1 != 2 : Not equal - -tests/test_unittest_two.py:11: AssertionErrorself = <test_unittest_two.Test_test2 testMethod=test_D2> - - def test_D2(self): -> raise ArithmeticError() -E ArithmeticError - -tests/test_unittest_two.py:14: ArithmeticErrorself = <test_unittest_two.Test_test2a testMethod=test_222A2> - - def test_222A2(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -tests/test_unittest_two.py:19: AssertionErrorself = <unittest_three_test.Test_test3 testMethod=test_A> - - def test_A(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -tests/unittest_three_test.py:6: AssertionError 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 2d7f12f87c68..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/three.output +++ /dev/null @@ -1,54 +0,0 @@ -============================= test session starts ============================== -platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 -rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard, inifile: -plugins: pylama-7.4.3 -collected 29 items - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -========================= no tests ran in 0.04 seconds ========================= - 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 @@ -non_parametrized_username = 'three' - - def test_parametrized_username(non_parametrized_username): -> assert non_parametrized_username in ['one', 'two', 'threes'] -E AssertionError: assert 'three' in ['one', 'two', 'threes'] - -tests/test_another_pytest.py:17: AssertionError 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 @@ -self = <test_root.Test_Root_test1 testMethod=test_Root_A> - - def test_Root_A(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -test_root.py:8: AssertionErrornon_parametrized_username = 'three' - - def test_parametrized_username(non_parametrized_username): -> assert non_parametrized_username in ['one', 'two', 'threes'] -E AssertionError: assert 'three' in ['one', 'two', 'threes'] - -tests/test_another_pytest.py:17: AssertionErrorself = <tests.external.ForeignTests.TestExtraNestedForeignTests object at 0x10fb685c0> - - def test_super_deep_foreign(self): -> assert False -E AssertionError - -tests/external.py:4: AssertionErrorself = <tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere object at 0x10fb74898> - - def test_foreign_test(self): -> assert False -E AssertionError - -tests/external.py:6: AssertionError/Users/donjayamanne/anaconda3/lib/python3.6/site-packages/_pytest/nose.py:23: <py._xmlgen.raw object at 0x1024fb518>non_parametrized_username = 'three' - - def test_parametrized_username(non_parametrized_username): -> assert non_parametrized_username in ['one', 'two', 'threes'] -E AssertionError: assert 'three' in ['one', 'two', 'threes'] - -tests/test_pytest.py:40: AssertionErrornon_parametrized_username = 'three' - - def test_parametrized_username(non_parametrized_username): -> assert non_parametrized_username in ['one', 'two', 'threes'] -E AssertionError: assert 'three' in ['one', 'two', 'threes'] - -tests/test_pytest.py:40: AssertionErrorself = <test_unittest_one.Test_test1 testMethod=test_A> - - def test_A(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -tests/test_unittest_one.py:8: AssertionErrorself = <test_unittest_two.Test_test2 testMethod=test_A2> - - def test_A2(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -tests/test_unittest_two.py:5: AssertionErrorself = <test_unittest_two.Test_test2 testMethod=test_C2> - - def test_C2(self): -> self.assertEqual(1,2,'Not equal') -E AssertionError: 1 != 2 : Not equal - -tests/test_unittest_two.py:11: AssertionErrorself = <test_unittest_two.Test_test2 testMethod=test_D2> - - def test_D2(self): -> raise ArithmeticError() -E ArithmeticError - -tests/test_unittest_two.py:14: ArithmeticErrorself = <unittest_three_test.Test_test3 testMethod=test_A> - - def test_A(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -tests/unittest_three_test.py:6: AssertionError 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 e2d04ab76d68..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/two.output +++ /dev/null @@ -1,64 +0,0 @@ -============================= test session starts ============================== -platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 -rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard, inifile: -plugins: pylama-7.4.3 -collected 33 items - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -========================= no tests ran in 0.06 seconds ========================= 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 @@ -self = <test_root.Test_Root_test1 testMethod=test_Root_A> - - def test_Root_A(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -test_root.py:8: AssertionErrortest_root.py:12: <py._xmlgen.raw object at 0x1024cf048>non_parametrized_username = 'three' - - def test_parametrized_username(non_parametrized_username): -> assert non_parametrized_username in ['one', 'two', 'threes'] -E AssertionError: assert 'three' in ['one', 'two', 'threes'] - -tests/test_another_pytest.py:17: AssertionErrorself = <tests.external.ForeignTests.TestExtraNestedForeignTests object at 0x10fb685c0> - - def test_super_deep_foreign(self): -> assert False -E AssertionError - -tests/external.py:4: AssertionErrorself = <tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere object at 0x10fb74898> - - def test_foreign_test(self): -> assert False -E AssertionError - -tests/external.py:6: AssertionError/Users/donjayamanne/anaconda3/lib/python3.6/site-packages/_pytest/nose.py:23: <py._xmlgen.raw object at 0x1024fb518>non_parametrized_username = 'three' - - def test_parametrized_username(non_parametrized_username): -> assert non_parametrized_username in ['one', 'two', 'threes'] -E AssertionError: assert 'three' in ['one', 'two', 'threes'] - -tests/test_pytest.py:40: AssertionErrorself = <test_unittest_one.Test_test1 testMethod=test_A> - - def test_A(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -tests/test_unittest_one.py:8: AssertionErrortests/test_unittest_one.py:12: <py._xmlgen.raw object at 0x102504cc0>self = <test_unittest_two.Test_test2 testMethod=test_A2> - - def test_A2(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -tests/test_unittest_two.py:5: AssertionErrorself = <test_unittest_two.Test_test2 testMethod=test_C2> - - def test_C2(self): -> self.assertEqual(1,2,'Not equal') -E AssertionError: 1 != 2 : Not equal - -tests/test_unittest_two.py:11: AssertionErrorself = <test_unittest_two.Test_test2 testMethod=test_D2> - - def test_D2(self): -> raise ArithmeticError() -E ArithmeticError - -tests/test_unittest_two.py:14: ArithmeticErrorself = <test_unittest_two.Test_test2a testMethod=test_222A2> - - def test_222A2(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -tests/test_unittest_two.py:19: AssertionErrorself = <unittest_three_test.Test_test3 testMethod=test_A> - - def test_A(self): -> self.fail("Not implemented") -E AssertionError: Not implemented - -tests/unittest_three_test.py:6: AssertionError 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/unitestsWithConfigs/other/test_pytest.py b/src/test/pythonFiles/testFiles/unitestsWithConfigs/other/test_pytest.py deleted file mode 100644 index dc5798306bb6..000000000000 --- a/src/test/pythonFiles/testFiles/unitestsWithConfigs/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/unitestsWithConfigs/other/test_unittest_one.py b/src/test/pythonFiles/testFiles/unitestsWithConfigs/other/test_unittest_one.py deleted file mode 100644 index e869986b6ead..000000000000 --- a/src/test/pythonFiles/testFiles/unitestsWithConfigs/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/unitestsWithConfigs/pytest.ini b/src/test/pythonFiles/testFiles/unitestsWithConfigs/pytest.ini deleted file mode 100644 index 45c88355be9d..000000000000 --- a/src/test/pythonFiles/testFiles/unitestsWithConfigs/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/unitestsWithConfigs/test_root.py b/src/test/pythonFiles/testFiles/unitestsWithConfigs/test_root.py deleted file mode 100644 index 452813e9a079..000000000000 --- a/src/test/pythonFiles/testFiles/unitestsWithConfigs/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/unitestsWithConfigs/tests/test_another_pytest.py b/src/test/pythonFiles/testFiles/unitestsWithConfigs/tests/test_another_pytest.py deleted file mode 100644 index 129bc168f0d5..000000000000 --- a/src/test/pythonFiles/testFiles/unitestsWithConfigs/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/unitestsWithConfigs/tests/test_pytest.py b/src/test/pythonFiles/testFiles/unitestsWithConfigs/tests/test_pytest.py deleted file mode 100644 index dc5798306bb6..000000000000 --- a/src/test/pythonFiles/testFiles/unitestsWithConfigs/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/unitestsWithConfigs/tests/test_unittest_one.py b/src/test/pythonFiles/testFiles/unitestsWithConfigs/tests/test_unittest_one.py deleted file mode 100644 index e869986b6ead..000000000000 --- a/src/test/pythonFiles/testFiles/unitestsWithConfigs/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/unitestsWithConfigs/tests/test_unittest_two.py b/src/test/pythonFiles/testFiles/unitestsWithConfigs/tests/test_unittest_two.py deleted file mode 100644 index ad89d873e879..000000000000 --- a/src/test/pythonFiles/testFiles/unitestsWithConfigs/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/unitestsWithConfigs/tests/unittest_three_test.py b/src/test/pythonFiles/testFiles/unitestsWithConfigs/tests/unittest_three_test.py deleted file mode 100644 index 507e6af02063..000000000000 --- a/src/test/pythonFiles/testFiles/unitestsWithConfigs/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/python_files/datascience/simple_nb.ipynb b/src/test/python_files/datascience/simple_nb.ipynb new file mode 100644 index 000000000000..bebbed6c7cd4 --- /dev/null +++ b/src/test/python_files/datascience/simple_nb.ipynb @@ -0,0 +1,41 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "with open('ds_n.log', 'a') as fp:\n", + " fp.write('Hello World')\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "nbformat": 4, + "nbformat_minor": 2, + "metadata": { + "language_info": { + "name": "python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "version": "3.7.4" + }, + "orig_nbformat": 2, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "npconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": 3 + } +} \ No newline at end of file diff --git a/src/test/python_files/datascience/simple_note_book.py b/src/test/python_files/datascience/simple_note_book.py new file mode 100644 index 000000000000..ace41e3f5c44 --- /dev/null +++ b/src/test/python_files/datascience/simple_note_book.py @@ -0,0 +1,7 @@ +# %% +import os.path +dir_path = os.path.dirname(os.path.realpath(__file__)) + +with open(os.path.join(dir_path, 'ds.log'), 'a') as fp: + fp.write('Hello World') + diff --git a/src/test/pythonFiles/debugging/forever.py b/src/test/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/python_files/debugging/wait_for_file.py b/src/test/python_files/debugging/wait_for_file.py new file mode 100644 index 000000000000..72dc90bda61e --- /dev/null +++ b/src/test/python_files/debugging/wait_for_file.py @@ -0,0 +1,35 @@ +import os.path +import sys +import time + + +try: + _, filename = sys.argv +except ValueError: + _, filename, outfile = sys.argv + sys.stdout = open(outfile, 'w') +print('waiting for file {!r}'.format(filename)) + +# We use sys.stdout.write() instead of print() because Python 2... + +if not os.path.exists(filename): + time.sleep(0.1) + sys.stdout.write('.') + sys.stdout.flush() +i = 1 +while not os.path.exists(filename): + time.sleep(0.1) + if i % 10 == 0: + sys.stdout.write(' ') + if i % 600 == 0: + if i == 600: + sys.stdout.write('\n = 1 minute =\n') + else: + sys.stdout.write('\n = {} minutes =\n'.format(i // 600)) + elif i % 100 == 0: + sys.stdout.write('\n') + sys.stdout.write('.') + sys.stdout.flush() + i += 1 +print('\nfound file {!r}'.format(filename)) +print('done!') diff --git a/src/test/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/python_files/terminalExec/sample7_normalized_selection.py b/src/test/python_files/terminalExec/sample7_normalized_selection.py new file mode 100644 index 000000000000..2288800fc985 --- /dev/null +++ b/src/test/python_files/terminalExec/sample7_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/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/python_files/terminalExec/sample8_raw.py b/src/test/python_files/terminalExec/sample8_raw.py new file mode 100644 index 000000000000..7920e6bce0d3 --- /dev/null +++ b/src/test/python_files/terminalExec/sample8_raw.py @@ -0,0 +1,9 @@ + if True: + print(1) + + print(1) + else: + print(2) + + print(2) + print(3) diff --git a/src/test/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 d475f41fa0c5..000000000000 --- a/src/test/refactor/extension.refactor.extract.method.test.ts +++ /dev/null @@ -1,135 +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 { extractMethod } from '../../client/providers/simpleRefactorProvider'; -import { RefactorProxy } from '../../client/refactor/proxy'; -import { getExtensionSettings } from '../common'; -import { UnitTestIocContainer } from '../unittests/serviceRegistry'; -import { closeActiveWindows, initialize, initializeTest } from './../initialize'; -import { MockOutputChannel } from './../mockClasses'; - -const EXTENSION_DIR = path.join(__dirname, '..', '..', '..'); -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) => 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(); - } - - async function testingMethodExtraction(shouldError: boolean, startPos: Position, endPos: Position): Promise { - const pythonSettings = getExtensionSettings(Uri.file(refactorTargetFile)); - const rangeOfTextToExtract = new Range(startPos, endPos); - const proxy = new RefactorProxy(EXTENSION_DIR, pythonSettings, path.dirname(refactorTargetFile), ioc.serviceContainer); - - // 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(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 { - 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(EXTENSION_DIR, 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 753b6ba16307..000000000000 --- a/src/test/refactor/extension.refactor.extract.var.test.ts +++ /dev/null @@ -1,144 +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 { extractVariable } from '../../client/providers/simpleRefactorProvider'; -import { RefactorProxy } from '../../client/refactor/proxy'; -import { getExtensionSettings, isPythonVersion } from '../common'; -import { UnitTestIocContainer } from '../unittests/serviceRegistry'; -import { closeActiveWindows, initialize, initializeTest, IS_CI_SERVER } from './../initialize'; -import { MockOutputChannel } from './../mockClasses'; - -const EXTENSION_DIR = path.join(__dirname, '..', '..', '..'); -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(); - (commands).executeCommand = (cmd) => 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(); - } - - async function testingVariableExtraction(shouldError: boolean, startPos: Position, endPos: Position): Promise { - const pythonSettings = getExtensionSettings(Uri.file(refactorTargetFile)); - const rangeOfTextToExtract = new Range(startPos, endPos); - const proxy = new RefactorProxy(EXTENSION_DIR, pythonSettings, path.dirname(refactorTargetFile), ioc.serviceContainer); - - 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(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 { - 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(EXTENSION_DIR, 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) - if (!IS_CI_SERVER) { - test('Extract Variable (end to end)', async () => { - 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 03bffd245899..000000000000 --- a/src/test/refactor/rename.test.ts +++ /dev/null @@ -1,75 +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 * as typeMoq from 'typemoq'; -import { Range, TextEditorCursorStyle, TextEditorLineNumbersStyle, TextEditorOptions, window, workspace } from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import '../../client/common/extensions'; -import { BufferDecoder } from '../../client/common/process/decoder'; -import { ProcessService } from '../../client/common/process/proc'; -import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; -import { IProcessServiceFactory, IPythonExecutionFactory } from '../../client/common/process/types'; -import { IConfigurationService, IPythonSettings } from '../../client/common/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { RefactorProxy } from '../../client/refactor/proxy'; -import { PYTHON_PATH } from '../common'; -import { closeActiveWindows, initialize, initializeTest } from './../initialize'; - -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; - let serviceContainer: typeMoq.IMock; - suiteSetup(initialize); - setup(async () => { - pythonSettings = typeMoq.Mock.ofType(); - pythonSettings.setup(p => p.pythonPath).returns(() => PYTHON_PATH); - const configService = typeMoq.Mock.ofType(); - configService.setup(c => c.getSettings(typeMoq.It.isAny())).returns(() => pythonSettings.object); - const processServiceFactory = typeMoq.Mock.ofType(); - processServiceFactory.setup(p => p.create(typeMoq.It.isAny())).returns(() => Promise.resolve(new ProcessService(new BufferDecoder()))); - - serviceContainer = typeMoq.Mock.ofType(); - 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(IPythonExecutionFactory), typeMoq.It.isAny())).returns(() => new PythonExecutionFactory(serviceContainer.object)); - await initializeTest(); - }); - teardown(closeActiveWindows); - suiteTeardown(closeActiveWindows); - - 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 proxy = new RefactorProxy(EXTENSION_ROOT_DIR, pythonSettings.object, path.dirname(sourceFile), serviceContainer.object); - const textDocument = await workspace.openTextDocument(sourceFile); - await window.showTextDocument(textDocument); - - const response = await proxy.rename(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 proxy = new RefactorProxy(EXTENSION_ROOT_DIR, pythonSettings.object, path.dirname(sourceFile), serviceContainer.object); - const textDocument = await workspace.openTextDocument(sourceFile); - await window.showTextDocument(textDocument); - - const response = await proxy.rename(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; + + let disposable: TypeMoq.IMock; + 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(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + disposable = TypeMoq.Mock.ofType(); + 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(); + sinon.stub(vscodeWorkspaceApis, 'onDidCloseNotebookDocument').callsFake((handler) => { + const disposable = notebookCloseEmitter.event(handler); + return disposable; + }); + + const mockPythonServer = { + onCodeExecuted: new EventEmitter().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; + let commandManager: TypeMoq.IMock; + let executionHelper: TypeMoq.IMock; + 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; + let disposableArray: Disposable[] = []; + + setup(() => { + interpreterService = TypeMoq.Mock.ofType(); + commandManager = TypeMoq.Mock.ofType(); + executionHelper = TypeMoq.Mock.ofType(); + commandManager + .setup((cm) => cm.registerCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => TypeMoq.Mock.ofType().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(); + 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); + 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); + 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); + 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); + 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; + let executeCommandStub: sinon.SinonStub; + let getActiveResourceStub: sinon.SinonStub; + + setup(() => { + interpreterService = TypeMoq.Mock.ofType(); + 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; + let notebook: TypeMoq.IMock; + let getConfigurationStub: sinon.SinonStub; + let configMock: TypeMoq.IMock; + let enabled: boolean; + const executionEventEmitter = new EventEmitter(); + 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(); + notebook = TypeMoq.Mock.ofType(); + provider = new VariablesProvider(varRequester.object, () => notebook.object, executionEventEmitter.event); + configMock = TypeMoq.Mock.ofType(); + configMock.setup((c) => c.get('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 8233f09f8b01..382659b3f838 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -2,48 +2,74 @@ // Licensed under the MIT License. import { Container } from 'inversify'; +import { anything } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; -import { Disposable, Memento, OutputChannel } from 'vscode'; -import { STANDARD_OUTPUT_CHANNEL } from '../client/common/constants'; -import { Logger } from '../client/common/logger'; -import { IS_WINDOWS } from '../client/common/platform/constants'; +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 { IFileSystem, IPlatformService } 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 } from '../client/common/process/types'; +import { + IProcessServiceFactory, + IPythonExecutionFactory, + IPythonToolExecutionService, +} from '../client/common/process/types'; import { registerTypes as commonRegisterTypes } from '../client/common/serviceRegistry'; -import { GLOBAL_MEMENTO, ICurrentProcess, IDisposableRegistry, ILogger, IMemento, IOutputChannel, IPathUtils, IsWindows, WORKSPACE_MEMENTO } from '../client/common/types'; +import { + GLOBAL_MEMENTO, + ICurrentProcess, + IDisposableRegistry, + IMemento, + IPathUtils, + IsWindows, + WORKSPACE_MEMENTO, + ILogOutputChannel, +} from '../client/common/types'; import { registerTypes as variableRegisterTypes } from '../client/common/variables/serviceRegistry'; -import { registerTypes as formattersRegisterTypes } from '../client/formatters/serviceRegistry'; -import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../client/interpreter/autoSelection/types'; -import { registerTypes as interpretersRegisterTypes } from '../client/interpreter/serviceRegistry'; +import { EnvironmentActivationService } from '../client/interpreter/activation/service'; +import { IEnvironmentActivationService } from '../client/interpreter/activation/types'; +import { + IInterpreterAutoSelectionService, + IInterpreterAutoSelectionProxyService, +} from '../client/interpreter/autoSelection/types'; +import { IInterpreterService } from '../client/interpreter/contracts'; +import { InterpreterService } from '../client/interpreter/interpreterService'; +import { registerInterpreterTypes } from '../client/interpreter/serviceRegistry'; import { ServiceContainer } from '../client/ioc/container'; import { ServiceManager } from '../client/ioc/serviceManager'; import { IServiceContainer, IServiceManager } from '../client/ioc/types'; -import { registerTypes as lintersRegisterTypes } from '../client/linters/serviceRegistry'; -import { TEST_OUTPUT_CHANNEL } from '../client/unittests/common/constants'; -import { registerTypes as unittestsRegisterTypes } from '../client/unittests/serviceRegistry'; +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'; +import { registerForIOC } from './pythonEnvironments/legacyIOC'; +import { createTypeMoq } from './mocks/helper'; export class IocContainer { + // This may be set (before any registration happens) to indicate + // whether or not IOC should depend on the VS Code API (e.g. the + // "vscode" module). So in "functional" tests, this should be set + // to "false". + public useVSCodeAPI = true; + public readonly serviceManager: IServiceManager; + public readonly serviceContainer: IServiceContainer; private disposables: Disposable[] = []; constructor() { - const cont = new Container(); + const cont = new Container({ skipBaseClassChecks: true }); this.serviceManager = new ServiceManager(cont); this.serviceContainer = new ServiceContainer(cont); @@ -54,73 +80,113 @@ export class IocContainer { const stdOutputChannel = new MockOutputChannel('Python'); this.disposables.push(stdOutputChannel); - this.serviceManager.addSingletonInstance(IOutputChannel, stdOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.serviceManager.addSingletonInstance(ILogOutputChannel, stdOutputChannel); const testOutputChannel = new MockOutputChannel('Python Test - UnitTests'); this.disposables.push(testOutputChannel); - this.serviceManager.addSingletonInstance(IOutputChannel, testOutputChannel, TEST_OUTPUT_CHANNEL); + this.serviceManager.addSingletonInstance(ILogOutputChannel, testOutputChannel); - this.serviceManager.addSingleton(IInterpreterAutoSelectionService, MockAutoSelectionService); - this.serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); + this.serviceManager.addSingleton( + IInterpreterAutoSelectionService, + MockAutoSelectionService, + ); + this.serviceManager.addSingleton( + IInterpreterAutoSelectionProxyService, + MockAutoSelectionService, + ); } - public async dispose() : Promise { - for (let i = 0; i < this.disposables.length; i += 1) { - const disposable = this.disposables[i]; + + public async dispose(): Promise { + for (const disposable of this.disposables) { if (disposable) { - // tslint:disable-next-line:no-any - const promise = disposable.dispose() as Promise; + const promise = disposable.dispose() as Promise; 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, PlatformService); - this.serviceManager.addSingleton(IFileSystem, FileSystem); + this.serviceManager.addSingleton( + IFileSystem, + // Maybe use fake vscode.workspace.filesystem API: + this.useVSCodeAPI ? FileSystem : LegacyFileSystem, + ); } - public registerProcessTypes() { + + public registerProcessTypes(): void { processRegisterTypes(this.serviceManager); + const mockEnvironmentActivationService = createTypeMoq(); + 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() { - interpretersRegisterTypes(this.serviceManager); + + 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, BufferDecoder); - const processServiceFactory = TypeMoq.Mock.ofType(); - // tslint:disable-next-line:no-any - const processService = new MockProcessService(new ProcessService(new BufferDecoder(), process.env as any)); - processServiceFactory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService)); - this.serviceManager.addSingletonInstance(IProcessServiceFactory, processServiceFactory.object); + + public registerMockProcessTypes(): void { + const processServiceFactory = createTypeMoq(); + + // 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, + processServiceFactory.object, + ); this.serviceManager.addSingleton(IPythonExecutionFactory, PythonExecutionFactory); - this.serviceManager.addSingleton(IPythonToolExecutionService, PythonToolExecutionService); + this.serviceManager.addSingleton( + IPythonToolExecutionService, + PythonToolExecutionService, + ); + this.serviceManager.addSingleton( + IEnvironmentActivationService, + EnvironmentActivationService, + ); + const mockEnvironmentActivationService = createTypeMoq(); + mockEnvironmentActivationService + .setup((m) => m.getActivatedEnvironmentVariables(anything())) + .returns(() => Promise.resolve(undefined)); + this.serviceManager.rebindInstance( + IEnvironmentActivationService, + mockEnvironmentActivationService.object, + ); + } + + public async registerMockInterpreterTypes(): Promise { + this.serviceManager.addSingleton(IInterpreterService, InterpreterService); + this.serviceManager.addSingleton(IRegistry, RegistryImplementation); + await registerForIOC(this.serviceManager, this.serviceContainer); } - public registerMockProcess() { - this.serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); + public registerMockProcess(): void { + this.serviceManager.addSingletonInstance(IsWindows, isWindows()); - this.serviceManager.addSingleton(ILogger, Logger); this.serviceManager.addSingleton(IPathUtils, PathUtils); this.serviceManager.addSingleton(ICurrentProcess, MockProcess); } diff --git a/src/test/smoke/_run_first_msLanguageServer.smoke.test.ts b/src/test/smoke/_run_first_msLanguageServer.smoke.test.ts deleted file mode 100644 index c409d63c459c..000000000000 --- a/src/test/smoke/_run_first_msLanguageServer.smoke.test.ts +++ /dev/null @@ -1,61 +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, initializeTest } from '../initialize'; -import { enableJedi, initializeSmokeTests, openFileAndWaitForLS } from './common'; - -const fileDefinitions = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', 'definitions.py'); - -suite('Smoke Test: Language Server', function () { - // Large value to allow for LS to get downloaded. - this.timeout(4 * 60000); - - suiteSetup(async function () { - if (!IS_SMOKE_TEST) { - return this.skip(); - } - await updateSetting('linting.ignorePatterns', ['**/dir1/**'], vscode.workspace.workspaceFolders![0].uri, vscode.ConfigurationTarget.WorkspaceFolder); - await initializeSmokeTests(); - }); - setup(async () => { - await initializeTest(); - await closeActiveWindows(); - }); - suiteTeardown(async () => { - await enableJedi(undefined); - 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.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/common.ts b/src/test/smoke/common.ts index 0b8bfe2e62a5..5f5b691fb496 100644 --- a/src/test/smoke/common.ts +++ b/src/test/smoke/common.ts @@ -3,74 +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 { waitForCondition } from '../common'; -import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST, SMOKE_TEST_EXTENSIONS_DIR } from '../constants'; +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'; -import { initialize } from '../initialize'; - -let initialized = false; -const fileDefinitions = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', 'definitions.py'); -export async function initializeSmokeTests() { - if (!IS_SMOKE_TEST || initialized) { - return; - } - await removeLanguageServerFiles(); - await enableJedi(false); - await initialize(); - await openFileAndWaitForLS(fileDefinitions); - initialized = true; -} - -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 { const resource = vscode.workspace.workspaceFolders![0].uri; - await vscode.workspace.getConfiguration('python', resource).update(setting, value, vscode.ConfigurationTarget.WorkspaceFolder); + await vscode.workspace + .getConfiguration('python', resource) + .update(setting, value, vscode.ConfigurationTarget.WorkspaceFolder); } -export async function removeLanguageServerFiles() { - const folders = await getLanaguageServerFolders(); - await Promise.all(folders.map(item => fs.remove(item).catch(noop))); +export async function removeLanguageServerFiles(): Promise { + const folders = await getLanguageServerFolders(); + await Promise.all(folders.map((item) => fs.remove(item).catch(noop))); } -async function getLanaguageServerFolders(): Promise { +async function getLanguageServerFolders(): Promise { return new Promise((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('jediEnabled') === true; + return settings.get('languageServer') === 'Jedi'; } -export async function enableJedi(enable: boolean | undefined) { +export async function enableJedi(enable: boolean | undefined): Promise { if (isJediEnabled() === enable) { return; } - await updateSetting('jediEnabled', enable); + await updateSetting('languageServer', 'Jedi'); +} + +export async function openNotebook(file: string): Promise { + 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 { + 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', + 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 { - const textDocument = await vscode.workspace.openTextDocument(file); - await vscode.window.showTextDocument(textDocument); - assert(vscode.window.activeTextEditor, 'No active editor'); + 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.executeCompletionItemProvider', textDocument.uri, new vscode.Position(0, 0)); - await waitForCondition(isLanguageServerDownloaded, 30_000, 'Language Server not downloaded'); + await vscode.commands + .executeCommand( + '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; } -async function isLanguageServerDownloaded() { - // tslint:disable-next-line:no-unnecessary-local-variable - const downloaded = await getLanaguageServerFolders().then(items => items.length > 0); - return downloaded; +export async function verifyExtensionIsAvailable(extensionId: string): Promise { + 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 new file mode 100644 index 000000000000..9f4421de4676 --- /dev/null +++ b/src/test/smoke/datascience.smoke.test.ts @@ -0,0 +1,91 @@ +// 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 * 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: Datascience', () => { + suiteSetup(async function () { + 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); + teardown(closeActiveWindows); + + test('Run Cell in interactive window', async () => { + const file = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'python_files', + 'datascience', + 'simple_note_book.py', + ); + const outputFile = path.join(path.dirname(file), 'ds.log'); + if (await fs.pathExists(outputFile)) { + await fs.unlink(outputFile); + } + const textDocument = await openFile(file); + + // Wait for code lenses to get detected. + await sleep(1_000); + + await vscode.commands.executeCommand('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); + + test('Run Cell in native editor', async () => { + const file = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'test', + 'python_files', + 'datascience', + 'simple_nb.ipynb', + ); + const fileContents = await fs.readFile(file, { encoding: 'utf-8' }); + const outputFile = path.join(path.dirname(file), 'ds_n.log'); + await fs.writeFile(file, fileContents.replace("'ds_n.log'", `'${outputFile.replace(/\\/g, '/')}'`), { + encoding: 'utf-8', + }); + if (await fs.pathExists(outputFile)) { + await fs.unlink(outputFile); + } + + 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('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`); + + // Give time for the file to be saved before we shutdown + await sleep(300); + }).timeout(timeoutForCellToRun); +}); diff --git a/src/test/smoke/debugger.smoke.test.ts b/src/test/smoke/debugger.smoke.test.ts deleted file mode 100644 index 1d4f0c08c314..000000000000 --- a/src/test/smoke/debugger.smoke.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 no-invalid-this no-any - -import { expect } from 'chai'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { openFile, waitForCondition } from '../common'; -import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; -import { closeActiveWindows, initializeTest } from '../initialize'; -import { initializeSmokeTests } from './common'; - -suite('Smoke Test: Debug file', function () { - // Large value to allow for LS to get downloaded. - this.timeout(4 * 60_000); - - suiteSetup(async function () { - if (!IS_SMOKE_TEST) { - return this.skip(); - } - await initializeSmokeTests(); - }); - setup(initializeTest); - suiteTeardown(closeActiveWindows); - teardown(closeActiveWindows); - - test('Debug', async () => { - const file = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', 'testExecInTerminal.py'); - const outputFile = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', 'testExecInTerminal.log'); - if (await fs.pathExists(outputFile)) { - await fs.unlink(outputFile); - } - await openFile(file); - - const config = { - name: 'Debug', - request: 'launch', - type: 'python', - program: file - }; - - const started = await vscode.debug.startDebugging(vscode.workspace.workspaceFolders![0], config); - expect(started).to.be.equal(true, 'Debugger did not sart'); - const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); - await waitForCondition(checkIfFileHasBeenCreated, 30_000, '\'testExecInTerminal.log\' 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/runInTerminal.smoke.test.ts b/src/test/smoke/runInTerminal.smoke.test.ts index 7b5ca203f711..4bdec0843862 100644 --- a/src/test/smoke/runInTerminal.smoke.test.ts +++ b/src/test/smoke/runInTerminal.smoke.test.ts @@ -3,40 +3,59 @@ '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, initializeTest } from '../initialize'; -import { initializeSmokeTests } from './common'; - -suite('Smoke Test: Run Python File In Terminal', function () { - // Large value to allow for LS to get downloaded. - this.timeout(4 * 60_000); +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +suite('Smoke Test: Run Python File In Terminal', () => { suiteSetup(async function () { if (!IS_SMOKE_TEST) { return this.skip(); } - await initializeSmokeTests(); + 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 () => { - const file = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', 'testExecInTerminal.py'); - const outputFile = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', 'testExecInTerminal.log'); + // 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', + ); + const outputFile = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'testExecInTerminal.log', + ); if (await fs.pathExists(outputFile)) { await fs.unlink(outputFile); } const textDocument = await openFile(file); - await vscode.commands.executeCommand('python.execInTerminal', textDocument.uri); + await vscode.commands.executeCommand('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, '\'testExecInTerminal.log\' file not created'); + 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('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('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + await vscode.commands + .executeCommand('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((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 263cb200734e..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'; @@ -17,36 +14,61 @@ import { EXTENSION_ROOT_DIR_FOR_TESTS, SMOKE_TEST_EXTENSIONS_DIR } from './const class TestRunner { public async start() { - await this.enableLanguageServer(true); + console.log('Start Test Runner'); + await this.enableLanguageServer(); await this.extractLatestExtension(SMOKE_TEST_EXTENSIONS_DIR); await this.launchSmokeTests(); } - private async launchSmokeTests() { - const env: { [key: string]: {} } = { + private async launchSmokeTests() { + const env: Record = { 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) { - const settings = `{ "python.jediEnabled": ${!enable} }`; - await fs.ensureDir(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', '.vscode')); - await fs.writeFile(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', '.vscode', 'settings.json'), settings); + private async enableLanguageServer() { + // When running smoke tests, we won't have access to unbundled files. + const settings = `{ "python.languageServer": "Jedi" }`; + await fs.ensureDir( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', '.vscode'), + ); + await fs.writeFile( + path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + '.vscode', + 'settings.json', + ), + settings, + ); } - private async launchTest(customEnvVars: { [key: string]: {} }) { - await new Promise((resolve, reject) => { - const env: { [key: string]: {} } = { + private async launchTest(customEnvVars: Record) { + console.log('Launch tests in test runner'); + await new Promise((resolve, reject) => { + const env: Record = { TEST_FILES_SUFFIX: 'smoke.test', - CODE_TESTS_WORKSPACE: path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests'), + IS_SMOKE_TEST: 'true', + CODE_TESTS_WORKSPACE: path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + ), ...process.env, - ...customEnvVars + ...customEnvVars, }; - const proc = spawn('node', [path.join(__dirname, 'standardTest.js')], { cwd: EXTENSION_ROOT_DIR_FOR_TESTS, env }); + const proc = spawn('node', [path.join(__dirname, 'standardTest.js')], { + cwd: EXTENSION_ROOT_DIR_FOR_TESTS, + env, + }); proc.stdout.pipe(process.stdout); proc.stderr.pipe(process.stderr); proc.on('error', reject); - proc.on('close', code => { + proc.on('exit', (code) => { + console.log(`Tests Exited with code ${code}`); if (code === 0) { resolve(); } else { @@ -57,9 +79,15 @@ class TestRunner { } private async extractLatestExtension(targetDir: string): Promise { - const extensionFile = await new Promise((resolve, reject) => glob('*.vsix', (ex, files) => ex ? reject(ex) : resolve(files[0]))); + const extensionFile = await new Promise((resolve, reject) => + glob.default('*.vsix', (ex, files) => (ex ? reject(ex) : resolve(files[0]))), + ); await unzip(extensionFile, targetDir); } } -new TestRunner().start().catch(ex => console.error('Error in running Smoke Tests', ex)); +new TestRunner().start().catch((ex) => { + console.error('Error in running Smoke Tests', ex); + // Exit with non zero exit code, so CI fails. + process.exit(1); +}); diff --git a/src/test/sourceMapSupport.unit.test.ts b/src/test/sourceMapSupport.unit.test.ts deleted file mode 100644 index be33b5027f28..000000000000 --- a/src/test/sourceMapSupport.unit.test.ts +++ /dev/null @@ -1,81 +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 { ConfigurationTarget } from 'vscode'; -import { Diagnostics } from '../client/common/utils/localize'; -import * as sourceMaps 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: { - 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 }; - } - test('Test message is not displayed when source maps are not enabled', async () => { - const stub = createVSCStub(false); - sourceMaps.default(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 not displayed when source maps are not enabled', async () => { - const stub = createVSCStub(true); - const instance = new class extends sourceMaps.SourceMapSupport { - protected initializeSourceMaps() { - noop(); - } - }(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(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 sourceMaps.SourceMapSupport { - protected initializeSourceMaps() { - noop(); - } - }(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'); - }); -}); diff --git a/src/test/standardTest.ts b/src/test/standardTest.ts index 38b24400b0d4..c3a7968c9c7a 100644 --- a/src/test/standardTest.ts +++ b/src/test/standardTest.ts @@ -1,14 +1,107 @@ -// tslint:disable:no-console no-require-imports no-var-requires - +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 { 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 { getChannel } from './utils/vscode'; +import { TestOptions } from '@vscode/test-electron/out/runTest'; + +// 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.CODE_TESTS_WORKSPACE = process.env.CODE_TESTS_WORKSPACE ? process.env.CODE_TESTS_WORKSPACE : path.join(__dirname, '..', '..', 'src', 'test'); process.env.IS_CI_SERVER_TEST_DEBUGGER = ''; process.env.VSC_PYTHON_CI_TEST = '1'; +const workspacePath = process.env.CODE_TESTS_WORKSPACE + ? process.env.CODE_TESTS_WORKSPACE + : path.join(__dirname, '..', '..', 'src', 'test'); +const extensionDevelopmentPath = process.env.CODE_EXTENSIONS_PATH + ? process.env.CODE_EXTENSIONS_PATH + : EXTENSION_ROOT_DIR_FOR_TESTS; + +/** + * 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', + }); +} + +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); + } +} -function start() { +async function start() { console.log('*'.repeat(100)); console.log('Start Standard tests'); - require('../../node_modules/vscode/bin/test'); + 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'), + launchArgs, + version: channel, + 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/startupTelemetry.unit.test.ts b/src/test/startupTelemetry.unit.test.ts new file mode 100644 index 000000000000..a9af3adff9a5 --- /dev/null +++ b/src/test/startupTelemetry.unit.test.ts @@ -0,0 +1,51 @@ +// 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 { 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; + let experimentsManager: TypeMoq.IMock; + let interpreterPathService: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + experimentsManager = TypeMoq.Mock.ofType(); + interpreterPathService = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); + 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); + }); + + [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}}`, () => { + 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'); + }); + }); + }); + }); + + test('Return true if using setting value equals something else', () => { + interpreterPathService + .setup((i) => i.inspect(resource)) + .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 new file mode 100644 index 000000000000..99b6e0b38ceb --- /dev/null +++ b/src/test/telemetry/envFileTelemetry.unit.test.ts @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { anyString, instance, mock, when } from 'ts-mockito'; +import { Uri } from 'vscode'; +import { IWorkspaceService } from '../../client/common/application/types'; +import { WorkspaceService } from '../../client/common/application/workspace'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../client/common/platform/types'; +import * as Telemetry from '../../client/telemetry'; +import { EventName } from '../../client/telemetry/constants'; +import { + EnvFileTelemetryTests, + sendActivationTelemetry, + sendFileCreationTelemetry, + sendSettingTelemetry, +} from '../../client/telemetry/envFileTelemetry'; + +suite('Env file telemetry', () => { + const defaultEnvFileValue = 'someDefaultValue'; + const resource = Uri.parse('foo'); + + let telemetryEvent: { eventName: EventName; hasCustomEnvPath: boolean } | undefined; + let sendTelemetryStub: sinon.SinonStub; + let workspaceService: IWorkspaceService; + let fileSystem: IFileSystem; + + setup(() => { + fileSystem = mock(FileSystem); + workspaceService = mock(WorkspaceService); + + const mockWorkspaceConfig = { + inspect: () => ({ + defaultValue: defaultEnvFileValue, + }), + }; + + when(workspaceService.getConfiguration('python')).thenReturn(mockWorkspaceConfig as any); + + const mockSendTelemetryEvent = (( + eventName: EventName, + _: number | undefined, + { hasCustomEnvPath }: { hasCustomEnvPath: boolean }, + ) => { + telemetryEvent = { + eventName, + hasCustomEnvPath, + }; + }) as typeof Telemetry.sendTelemetryEvent; + + sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + }); + + teardown(() => { + telemetryEvent = undefined; + sinon.restore(); + EnvFileTelemetryTests.resetState(); + }); + + test('Setting telemetry should be sent with hasCustomEnvPath at true if the python.envFile setting is different from the default value', () => { + sendSettingTelemetry(instance(workspaceService), 'bar'); + + sinon.assert.calledOnce(sendTelemetryStub); + assert.deepEqual(telemetryEvent, { eventName: EventName.ENVFILE_WORKSPACE, hasCustomEnvPath: true }); + }); + + test('Setting telemetry should not be sent if a telemetry event has already been sent', () => { + EnvFileTelemetryTests.setState({ telemetrySent: true }); + + sendSettingTelemetry(instance(workspaceService), 'bar'); + + sinon.assert.notCalled(sendTelemetryStub); + assert.deepEqual(telemetryEvent, undefined); + }); + + test('Setting telemetry should not be sent if the python.envFile setting is the same as the default value', () => { + EnvFileTelemetryTests.setState({ defaultSetting: defaultEnvFileValue }); + + sendSettingTelemetry(instance(workspaceService), defaultEnvFileValue); + + sinon.assert.notCalled(sendTelemetryStub); + assert.deepEqual(telemetryEvent, undefined); + }); + + test('File creation telemetry should be sent if no telemetry event has been sent before', () => { + sendFileCreationTelemetry(); + + sinon.assert.calledOnce(sendTelemetryStub); + assert.deepEqual(telemetryEvent, { eventName: EventName.ENVFILE_WORKSPACE, hasCustomEnvPath: false }); + }); + + test('File creation telemetry should not be sent if a telemetry event has already been sent', () => { + EnvFileTelemetryTests.setState({ telemetrySent: true }); + + sendFileCreationTelemetry(); + + sinon.assert.notCalled(sendTelemetryStub); + assert.deepEqual(telemetryEvent, undefined); + }); + + test('Activation telemetry should be sent if no telemetry event has been sent before, and a .env file exists', async () => { + when(fileSystem.fileExists(anyString())).thenResolve(true); + + await sendActivationTelemetry(instance(fileSystem), instance(workspaceService), resource); + + sinon.assert.calledOnce(sendTelemetryStub); + assert.deepEqual(telemetryEvent, { eventName: EventName.ENVFILE_WORKSPACE, hasCustomEnvPath: false }); + }); + + test('Activation telemetry should not be sent if a telemetry event has already been sent', async () => { + EnvFileTelemetryTests.setState({ telemetrySent: true }); + + await sendActivationTelemetry(instance(fileSystem), instance(workspaceService), resource); + + sinon.assert.notCalled(sendTelemetryStub); + assert.deepEqual(telemetryEvent, undefined); + }); + + test('Activation telemetry should not be sent if no .env file exists', async () => { + when(fileSystem.fileExists(anyString())).thenResolve(false); + + await sendActivationTelemetry(instance(fileSystem), instance(workspaceService), resource); + + sinon.assert.notCalled(sendTelemetryStub); + assert.deepEqual(telemetryEvent, undefined); + }); +}); diff --git a/src/test/telemetry/extensionInstallTelemetry.unit.test.ts b/src/test/telemetry/extensionInstallTelemetry.unit.test.ts new file mode 100644 index 000000000000..47e25eca05fa --- /dev/null +++ b/src/test/telemetry/extensionInstallTelemetry.unit.test.ts @@ -0,0 +1,29 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { anyString, instance, mock, when } from 'ts-mockito'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { IFileSystem } from '../../client/common/platform/types'; +import * as Telemetry from '../../client/telemetry'; +import { setExtensionInstallTelemetryProperties } from '../../client/telemetry/extensionInstallTelemetry'; + +suite('Extension Install Telemetry', () => { + let fs: IFileSystem; + let telemetryPropertyStub: sinon.SinonStub; + setup(() => { + fs = mock(FileSystem); + telemetryPropertyStub = sinon.stub(Telemetry, 'setSharedProperty'); + }); + teardown(() => { + telemetryPropertyStub.restore(); + }); + test('PythonCodingPack exists', async () => { + when(fs.fileExists(anyString())).thenResolve(true); + await setExtensionInstallTelemetryProperties(instance(fs)); + assert.ok(telemetryPropertyStub.calledOnceWithExactly('installSource', 'pythonCodingPack')); + }); + test('PythonCodingPack does not exists', async () => { + when(fs.fileExists(anyString())).thenResolve(false); + await setExtensionInstallTelemetryProperties(instance(fs)); + assert.ok(telemetryPropertyStub.calledOnceWithExactly('installSource', 'marketPlace')); + }); +}); diff --git a/src/test/telemetry/index.unit.test.ts b/src/test/telemetry/index.unit.test.ts new file mode 100644 index 000000000000..d8a6b72eedc6 --- /dev/null +++ b/src/test/telemetry/index.unit.test.ts @@ -0,0 +1,143 @@ +// 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 * as fs from '../../client/common/platform/fs-paths'; + +import { + _resetSharedProperties, + clearTelemetryReporter, + sendTelemetryEvent, + setSharedProperty, +} from '../../client/telemetry'; + +suite('Telemetry', () => { + 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[] = []; + public static measures: {}[] = []; + public static exception: Error | undefined; + + public static clear() { + Reporter.eventName = []; + Reporter.properties = []; + Reporter.measures = []; + } + public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { + Reporter.eventName.push(eventName); + Reporter.properties.push(properties!); + Reporter.measures.push(measures!); + } + public sendTelemetryErrorEvent(eventName: string, properties?: {}, measures?: {}) { + this.sendTelemetryEvent(eventName, properties, measures); + } + public sendTelemetryException(_error: Error, _properties?: {}, _measures?: {}): void { + throw new Error('sendTelemetryException is unsupported'); + } + } + + setup(() => { + 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(); + }); + teardown(() => { + process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; + process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; + rewiremock.disable(); + _resetSharedProperties(); + sinon.restore(); + }); + + test('Send Telemetry', () => { + rewiremock.enable(); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); + + const eventName = 'Testing'; + const properties = { hello: 'world', foo: 'bar' }; + const measures = { start: 123, end: 987 }; + + sendTelemetryEvent(eventName as any, measures, properties as any); + + expect(Reporter.eventName).to.deep.equal([eventName]); + expect(Reporter.measures).to.deep.equal([measures]); + expect(Reporter.properties).to.deep.equal([properties]); + }); + test('Send Telemetry with no properties', () => { + rewiremock.enable(); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); + + const eventName = 'Testing'; + + sendTelemetryEvent(eventName as any); + + expect(Reporter.eventName).to.deep.equal([eventName]); + expect(Reporter.measures).to.deep.equal([undefined], 'Measures should be empty'); + expect(Reporter.properties).to.deep.equal([{}], 'Properties should be empty'); + }); + test('Send Telemetry with shared properties', () => { + rewiremock.enable(); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); + + const eventName = 'Testing'; + const properties = { hello: 'world', foo: 'bar' }; + const measures = { start: 123, end: 987 }; + const expectedProperties = { ...properties, one: 'two' }; + + setSharedProperty('one' as any, 'two' as any); + + sendTelemetryEvent(eventName as any, measures, properties as any); + + expect(Reporter.eventName).to.deep.equal([eventName]); + expect(Reporter.measures).to.deep.equal([measures]); + expect(Reporter.properties).to.deep.equal([expectedProperties]); + }); + test('Shared properties will replace existing ones', () => { + rewiremock.enable(); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); + + const eventName = 'Testing'; + const properties = { hello: 'world', foo: 'bar' }; + const measures = { start: 123, end: 987 }; + const expectedProperties = { ...properties, foo: 'baz' }; + + setSharedProperty('foo' as any, 'baz' as any); + + 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 Exception Telemetry', () => { + rewiremock.enable(); + const error = new Error('Boo'); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); + + const eventName = 'Testing'; + const measures = { start: 123, end: 987 }; + const properties = { hello: 'world', foo: 'bar' }; + + sendTelemetryEvent(eventName as any, measures, properties as any, error); + + const expectedProperties = { + ...properties, + errorName: error.name, + errorStack: error.stack, + }; + + expect(Reporter.eventName).to.deep.equal([eventName]); + expect(Reporter.properties).to.deep.equal([expectedProperties]); + expect(Reporter.measures).to.deep.equal([measures]); + }); +}); diff --git a/src/test/terminals/activation.unit.test.ts b/src/test/terminals/activation.unit.test.ts new file mode 100644 index 000000000000..4c5294a82f49 --- /dev/null +++ b/src/test/terminals/activation.unit.test.ts @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { anything, instance, mock, verify, when } from 'ts-mockito'; +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, ITerminalManager } from '../../client/common/application/types'; +import { TerminalActivator } from '../../client/common/terminal/activator'; +import { ITerminalActivator } from '../../client/common/terminal/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'; + +suite('Terminal', () => { + suite('Terminal Auto Activation', () => { + let autoActivation: ITerminalAutoActivation; + let manager: ITerminalManager; + let activator: ITerminalActivator; + let resourceService: IActiveResourceService; + let onDidOpenTerminalEventEmitter: EventEmitter; + 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); + onDidOpenTerminalEventEmitter = new EventEmitter(); + when(manager.onDidOpenTerminal).thenReturn(onDidOpenTerminalEventEmitter.event); + when(activator.activateEnvironmentInTerminal(anything(), anything())).thenResolve(); + + autoActivation = new TerminalAutoActivation( + instance(manager), + [], + instance(activator), + instance(resourceService), + ); + + terminal = ({ + dispose: noop, + hide: noop, + name: 'Some Name', + creationOptions: {}, + processId: Promise.resolve(0), + sendText: noop, + show: noop, + exitStatus: { code: 0 }, + } as unknown) as Terminal; + nonActivatedTerminal = ({ + dispose: noop, + hide: noop, + creationOptions: { hideFromUser: true }, + name: 'Something', + processId: Promise.resolve(0), + sendText: noop, + show: noop, + exitStatus: { code: 0 }, + } as unknown) as Terminal; + autoActivation.register(); + }); + // teardown(() => fakeTimer.uninstall()); + teardown(() => { + sinon.restore(); + }); + + test('Should activate terminal', async () => { + // Trigger opening a terminal. + + await ((onDidOpenTerminalEventEmitter.fire(terminal) as unknown) as Promise); + + // 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. + + await ((onDidOpenTerminalEventEmitter.fire(nonActivatedTerminal) as unknown) as Promise); + + // The terminal should get activated. + verify(activator.activateEnvironmentInTerminal(anything(), anything())).never(); + }); + test('Should not activate terminal when envs extension should handle activation', async () => { + shouldEnvExtHandleActivationStub.returns(true); + + await ((onDidOpenTerminalEventEmitter.fire(terminal) as unknown) as Promise); + + 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 05c395a1e571..726b118ce180 100644 --- a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +++ b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -1,18 +1,21 @@ // 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 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 { 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-next-line:max-func-body-length suite('Terminal - Code Execution Manager', () => { let executionManager: ICodeExecutionManager; let workspace: TypeMoq.IMock; @@ -20,20 +23,47 @@ suite('Terminal - Code Execution Manager', () => { let disposables: Disposable[] = []; let serviceContainer: TypeMoq.IMock; let documentManager: TypeMoq.IMock; + let configService: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + workspace = TypeMoq.Mock.ofType(); - workspace.setup(c => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { - return { - dispose: () => void 0 - }; - }); + workspace + .setup((c) => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + return { + dispose: () => void 0, + }; + }); documentManager = TypeMoq.Mock.ofType(); - commandManager = TypeMoq.Mock.ofType(); + commandManager = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); serviceContainer = TypeMoq.Mock.ofType(); - executionManager = new CodeExecutionManager(commandManager.object, documentManager.object, disposables, serviceContainer.object); + configService = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + 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, + configService.object, + serviceContainer.object, + ); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); }); teardown(() => { - disposables.forEach(disposable => { + sinon.restore(); + disposables.forEach((disposable) => { if (disposable) { disposable.dispose(); } @@ -43,114 +73,144 @@ suite('Terminal - Code Execution Manager', () => { }); test('Ensure commands are registered', async () => { + const registered: string[] = []; + commandManager + .setup((c) => c.registerCommand) + .returns(() => { + return (command: string, _callback: (...args: any[]) => any, _thisArg?: any) => { + registered.push(command); + return { dispose: () => void 0 }; + }; + }); + executionManager.registerCommands(); - commandManager.verify(c => c.registerCommand(TypeMoq.It.isValue(Commands.Exec_In_Terminal), TypeMoq.It.isAny()), TypeMoq.Times.once()); - commandManager.verify(c => c.registerCommand(TypeMoq.It.isValue(Commands.Exec_Selection_In_Terminal), TypeMoq.It.isAny()), TypeMoq.Times.once()); - commandManager.verify(c => c.registerCommand(TypeMoq.It.isValue(Commands.Exec_Selection_In_Django_Shell), TypeMoq.It.isAny()), TypeMoq.Times.once()); + + const sorted = registered.sort(); + 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 () => { let commandHandler: undefined | (() => Promise); - // tslint:disable-next-line:no-any - commandManager.setup(c => c.registerCommand).returns(() => { - // tslint:disable-next-line:no-any - return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { - if (command === Commands.Exec_In_Terminal) { - commandHandler = callback; - } - return { dispose: () => void 0 }; - }; - }); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_Terminal) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); executionManager.registerCommands(); expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); const helper = TypeMoq.Mock.ofType(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); await commandHandler!(); - helper.verify(async h => h.getFileToExecute(), TypeMoq.Times.once()); + helper.verify(async (h) => h.getFileToExecute(), TypeMoq.Times.once()); }); test('Ensure executeFileInterTerminal will use provided file', async () => { let commandHandler: undefined | ((file: Uri) => Promise); - // tslint:disable-next-line:no-any - commandManager.setup(c => c.registerCommand).returns(() => { - // tslint:disable-next-line:no-any - return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { - if (command === Commands.Exec_In_Terminal) { - commandHandler = callback; - } - return { dispose: () => void 0 }; - }; - }); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_Terminal) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); executionManager.registerCommands(); expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); const helper = TypeMoq.Mock.ofType(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); const executionService = TypeMoq.Mock.ofType(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard'))).returns(() => executionService.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard'))) + .returns(() => executionService.object); const fileToExecute = Uri.file('x'); await commandHandler!(fileToExecute); - helper.verify(async h => h.getFileToExecute(), TypeMoq.Times.never()); - executionService.verify(async e => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + helper.verify(async (h) => h.getFileToExecute(), TypeMoq.Times.never()); + executionService.verify( + async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); test('Ensure executeFileInterTerminal will use active file', async () => { let commandHandler: undefined | ((file: Uri) => Promise); - // tslint:disable-next-line:no-any - commandManager.setup(c => c.registerCommand).returns(() => { - // tslint:disable-next-line:no-any - return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { - if (command === Commands.Exec_In_Terminal) { - commandHandler = callback; - } - return { dispose: () => void 0 }; - }; - }); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_Terminal) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); executionManager.registerCommands(); expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); const fileToExecute = Uri.file('x'); const helper = TypeMoq.Mock.ofType(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); - helper.setup(async h => h.getFileToExecute()).returns(() => Promise.resolve(fileToExecute)); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + helper.setup(async (h) => h.getFileToExecute()).returns(() => Promise.resolve(fileToExecute)); const executionService = TypeMoq.Mock.ofType(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard'))).returns(() => executionService.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard'))) + .returns(() => executionService.object); await commandHandler!(fileToExecute); - executionService.verify(async e => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + executionService.verify( + async (e) => e.executeFile(TypeMoq.It.isValue(fileToExecute), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); async function testExecutionOfSelectionWithoutAnyActiveDocument(commandId: string, executionSericeId: string) { let commandHandler: undefined | (() => Promise); - // tslint:disable-next-line:no-any - commandManager.setup(c => c.registerCommand).returns(() => { - // tslint:disable-next-line:no-any - return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { - if (command === commandId) { - commandHandler = callback; - } - return { dispose: () => void 0 }; - }; - }); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === commandId) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); executionManager.registerCommands(); expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); const helper = TypeMoq.Mock.ofType(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); const executionService = TypeMoq.Mock.ofType(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionSericeId))).returns(() => executionService.object); - documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionSericeId))) + .returns(() => executionService.object); + documentManager.setup((d) => d.activeTextEditor).returns(() => undefined); await commandHandler!(); - executionService.verify(async e => e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); + executionService.verify(async (e) => e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); } test('Ensure executeSelectionInTerminal will do nothing if theres no active document', async () => { @@ -163,30 +223,35 @@ suite('Terminal - Code Execution Manager', () => { async function testExecutionOfSlectionWithoutAnythingSelected(commandId: string, executionServiceId: string) { let commandHandler: undefined | (() => Promise); - // tslint:disable-next-line:no-any - commandManager.setup(c => c.registerCommand).returns(() => { - // tslint:disable-next-line:no-any - return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { - if (command === commandId) { - commandHandler = callback; - } - return { dispose: () => void 0 }; - }; - }); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === commandId) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); executionManager.registerCommands(); expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); const helper = TypeMoq.Mock.ofType(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); - helper.setup(h => h.getSelectedTextToExecute).returns(() => () => Promise.resolve('')); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + helper.setup((h) => h.getSelectedTextToExecute).returns(() => () => Promise.resolve('')); const executionService = TypeMoq.Mock.ofType(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionServiceId))).returns(() => executionService.object); - // tslint:disable-next-line:no-any - documentManager.setup(d => d.activeTextEditor).returns(() => { return {} as any; }); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionServiceId))) + .returns(() => executionService.object); + documentManager + .setup((d) => d.activeTextEditor) + .returns(() => { + return {} as any; + }); await commandHandler!(); - executionService.verify(async e => e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); + executionService.verify(async (e) => e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); } test('Ensure executeSelectionInTerminal will do nothing if no text is selected', async () => { @@ -199,16 +264,16 @@ suite('Terminal - Code Execution Manager', () => { async function testExecutionOfSelectionIsSentToTerminal(commandId: string, executionServiceId: string) { let commandHandler: undefined | (() => Promise); - // tslint:disable-next-line:no-any - commandManager.setup(c => c.registerCommand).returns(() => { - // tslint:disable-next-line:no-any - return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { - if (command === commandId) { - commandHandler = callback; - } - return { dispose: () => void 0 }; - }; - }); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === commandId) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); executionManager.registerCommands(); expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); @@ -216,19 +281,27 @@ suite('Terminal - Code Execution Manager', () => { const textSelected = 'abcd'; const activeDocumentUri = Uri.file('abc'); const helper = TypeMoq.Mock.ofType(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); - helper.setup(h => h.getSelectedTextToExecute).returns(() => () => Promise.resolve(textSelected)); - helper.setup(h => h.normalizeLines).returns(() => () => Promise.resolve(textSelected)).verifiable(TypeMoq.Times.once()); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + helper.setup((h) => h.getSelectedTextToExecute).returns(() => () => Promise.resolve(textSelected)); + helper + .setup((h) => h.normalizeLines) + .returns(() => () => Promise.resolve(textSelected)) + .verifiable(TypeMoq.Times.once()); const executionService = TypeMoq.Mock.ofType(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionServiceId))).returns(() => executionService.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionServiceId))) + .returns(() => executionService.object); const document = TypeMoq.Mock.ofType(); - document.setup(d => d.uri).returns(() => activeDocumentUri); + document.setup((d) => d.uri).returns(() => activeDocumentUri); const activeEditor = TypeMoq.Mock.ofType(); - activeEditor.setup(e => e.document).returns(() => document.object); - documentManager.setup(d => d.activeTextEditor).returns(() => activeEditor.object); + activeEditor.setup((e) => e.document).returns(() => document.object); + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); await commandHandler!(); - executionService.verify(async e => e.execute(TypeMoq.It.isValue(textSelected), TypeMoq.It.isValue(activeDocumentUri)), TypeMoq.Times.once()); + executionService.verify( + async (e) => e.execute(TypeMoq.It.isValue(textSelected), TypeMoq.It.isValue(activeDocumentUri)), + TypeMoq.Times.once(), + ); helper.verifyAll(); } test('Ensure executeSelectionInTerminal will normalize selected text and send it to the terminal', async () => { diff --git a/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts b/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts index e85da9496e88..749d94672765 100644 --- a/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts +++ b/src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts @@ -1,70 +1,109 @@ // 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'; +import { IProcessService, IPythonExecutionFactory } from '../../../client/common/process/types'; import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../../client/common/types'; import { DjangoShellCodeExecutionProvider } from '../../../client/terminals/codeExecution/djangoShellCodeExecution'; import { ICodeExecutionService } from '../../../client/terminals/types'; import { PYTHON_PATH } from '../../common'; +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; let terminalService: TypeMoq.IMock; let workspace: TypeMoq.IMock; let platform: TypeMoq.IMock; + let fileSystem: TypeMoq.IMock; let settings: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let pythonExecutionFactory: TypeMoq.IMock; + let applicationShell: TypeMoq.IMock; let disposables: Disposable[] = []; setup(() => { const terminalFactory = TypeMoq.Mock.ofType(); terminalSettings = TypeMoq.Mock.ofType(); terminalService = TypeMoq.Mock.ofType(); const configService = TypeMoq.Mock.ofType(); + applicationShell = TypeMoq.Mock.ofType(); workspace = TypeMoq.Mock.ofType(); - workspace.setup(c => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { - return { - dispose: () => void 0 - }; - }); + workspace + .setup((c) => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + return { + dispose: () => void 0, + }; + }); platform = TypeMoq.Mock.ofType(); const documentManager = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); const commandManager = TypeMoq.Mock.ofType(); - const fileSystem = TypeMoq.Mock.ofType(); - executor = new DjangoShellCodeExecutionProvider(terminalFactory.object, configService.object, - workspace.object, documentManager.object, platform.object, commandManager.object, fileSystem.object, disposables); + fileSystem = TypeMoq.Mock.ofType(); + pythonExecutionFactory = TypeMoq.Mock.ofType(); + executor = new DjangoShellCodeExecutionProvider( + terminalFactory.object, + configService.object, + workspace.object, + documentManager.object, + platform.object, + commandManager.object, + fileSystem.object, + disposables, + interpreterService.object, + applicationShell.object, + ); - terminalFactory.setup(f => f.getTerminalService(TypeMoq.It.isAny())).returns(() => terminalService.object); + terminalFactory.setup((f) => f.getTerminalService(TypeMoq.It.isAny())).returns(() => terminalService.object); settings = TypeMoq.Mock.ofType(); - settings.setup(s => s.terminal).returns(() => terminalSettings.object); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + settings.setup((s) => s.terminal).returns(() => terminalSettings.object); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); }); teardown(() => { - disposables.forEach(disposable => { + disposables.forEach((disposable) => { if (disposable) { disposable.dispose(); } }); disposables = []; + sinon.restore(); }); - function testReplCommandArguments(isWindows: boolean, pythonPath: string, expectedPythonPath: string, - terminalArgs: string[], expectedTerminalArgs: string[], resource?: Uri) { - platform.setup(p => p.isWindows).returns(() => isWindows); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + async function testReplCommandArguments( + isWindows: boolean, + pythonPath: string, + expectedPythonPath: string, + terminalArgs: string[], + expectedTerminalArgs: string[], + resource?: Uri, + ) { + platform.setup((p) => p.isWindows).returns(() => isWindows); + 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 = (executor as DjangoShellCodeExecutionProvider).getReplCommandArgs(resource); + const replCommandArgs = await (executor as DjangoShellCodeExecutionProvider).getExecutableInfo(resource); expect(replCommandArgs).not.to.be.an('undefined', 'Command args is undefined'); expect(replCommandArgs.command).to.be.equal(expectedPythonPath, 'Incorrect python path'); expect(replCommandArgs.args).to.be.deep.equal(expectedTerminalArgs, 'Incorrect arguments'); @@ -75,7 +114,13 @@ suite('Terminal - Django Shell Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); - testReplCommandArguments(true, pythonPath, 'c:/program files/python/python.exe', terminalArgs, expectedTerminalArgs); + await testReplCommandArguments( + true, + pythonPath, + 'c:/program files/python/python.exe', + terminalArgs, + expectedTerminalArgs, + ); }); test('Ensure fully qualified python path is returned as is, when building repl args on Windows', async () => { @@ -83,7 +128,7 @@ suite('Terminal - Django Shell Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); }); test('Ensure python path is returned as is, when building repl args on Windows', async () => { @@ -91,7 +136,7 @@ suite('Terminal - Django Shell Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); }); test('Ensure fully qualified python path is returned as is, on non Windows', async () => { @@ -99,7 +144,7 @@ suite('Terminal - Django Shell Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); }); test('Ensure python path is returned as is, on non Windows', async () => { @@ -107,7 +152,7 @@ suite('Terminal - Django Shell Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); }); test('Ensure current workspace folder (containing spaces) is used to prefix manage.py', async () => { @@ -115,10 +160,13 @@ suite('Terminal - Django Shell Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; const workspaceUri = Uri.file(path.join('c', 'usr', 'program files')); const workspaceFolder: WorkspaceFolder = { index: 0, name: 'blah', uri: workspaceUri }; - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); - const expectedTerminalArgs = terminalArgs.concat(`${path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument()}`, 'shell'); + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); + const expectedTerminalArgs = terminalArgs.concat( + `${path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgumentForPythonExt()}`, + 'shell', + ); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); }); test('Ensure current workspace folder (without spaces) is used to prefix manage.py', async () => { @@ -126,10 +174,13 @@ suite('Terminal - Django Shell Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; const workspaceUri = Uri.file(path.join('c', 'usr', 'programfiles')); const workspaceFolder: WorkspaceFolder = { index: 0, name: 'blah', uri: workspaceUri }; - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); - const expectedTerminalArgs = terminalArgs.concat(path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument(), 'shell'); + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); + const expectedTerminalArgs = terminalArgs.concat( + path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgumentForPythonExt(), + 'shell', + ); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); }); test('Ensure default workspace folder (containing spaces) is used to prefix manage.py', async () => { @@ -137,11 +188,14 @@ suite('Terminal - Django Shell Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; const workspaceUri = Uri.file(path.join('c', 'usr', 'program files')); const workspaceFolder: WorkspaceFolder = { index: 0, name: 'blah', uri: workspaceUri }; - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); - workspace.setup(w => w.workspaceFolders).returns(() => [workspaceFolder]); - const expectedTerminalArgs = terminalArgs.concat(`${path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument()}`, 'shell'); + 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').fileToCommandArgumentForPythonExt()}`, + 'shell', + ); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); }); test('Ensure default workspace folder (without spaces) is used to prefix manage.py', async () => { @@ -149,11 +203,74 @@ suite('Terminal - Django Shell Code Execution', () => { const terminalArgs = ['-a', 'b', 'c']; const workspaceUri = Uri.file(path.join('c', 'usr', 'programfiles')); const workspaceFolder: WorkspaceFolder = { index: 0, name: 'blah', uri: workspaceUri }; - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); - workspace.setup(w => w.workspaceFolders).returns(() => [workspaceFolder]); - const expectedTerminalArgs = terminalArgs.concat(path.join(workspaceUri.fsPath, 'manage.py').fileToCommandArgument(), 'shell'); + 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').fileToCommandArgumentForPythonExt(), + 'shell', + ); + + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + }); + + async function testReplCondaCommandArguments( + pythonPath: string, + terminalArgs: string[], + condaEnv: { name: string; path: string }, + resource?: Uri, + ) { + 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(); + 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, + execForLinter: procs.execForLinter, + }; + const expectedTerminalArgs = [...terminalArgs, 'manage.py', 'shell']; + pythonExecutionFactory + .setup((p) => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(condaExecutionService)); + + const replCommandArgs = await (executor as DjangoShellCodeExecutionProvider).getExecutableInfo(resource); - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + expect(replCommandArgs).not.to.be.an('undefined', 'Conda command args are undefined'); + expect(replCommandArgs.command).to.be.equal(pythonPath, 'Repl should use python not conda'); + expect(replCommandArgs.args).to.be.deep.equal(expectedTerminalArgs, 'Incorrect terminal arguments'); + } + + test('Ensure conda args including env name are passed when using a conda environment with a name', async () => { + const pythonPath = 'c:/program files/python/python.exe'; + const condaPath = { name: 'foo-env', path: 'path/to/foo-env' }; + const terminalArgs = ['-a', 'b', '-c']; + + await testReplCondaCommandArguments(pythonPath, terminalArgs, condaPath); }); + test('Ensure conda args including env path are passed when using a conda environment with an empty name', async () => { + const pythonPath = 'c:/program files/python/python.exe'; + const condaPath = { name: '', path: 'path/to/foo-env' }; + const terminalArgs = ['-a', 'b', '-c']; + + await testReplCondaCommandArguments(pythonPath, terminalArgs, condaPath); + }); }); diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts index 1562e611c576..b7e0d1617884 100644 --- a/src/test/terminals/codeExecution/helper.test.ts +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -4,264 +4,521 @@ '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 { + IProcessService, + IProcessServiceFactory, + ObservableExecutionResult, +} from '../../../client/common/process/types'; import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; -import { OSType } from '../../../client/common/utils/platform'; +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 { 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; let documentManager: TypeMoq.IMock; let applicationShell: TypeMoq.IMock; let helper: ICodeExecutionHelper; let document: TypeMoq.IMock; let editor: TypeMoq.IMock; let processService: TypeMoq.IMock; - let configService: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let commandManager: TypeMoq.IMock; + let workspaceService: TypeMoq.IMock; + let configurationService: TypeMoq.IMock; + let pythonSettings: TypeMoq.IMock; + 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', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, + }; + setup(() => { const serviceContainer = TypeMoq.Mock.ofType(); + commandManager = TypeMoq.Mock.ofType(); + configurationService = TypeMoq.Mock.ofType(); + workspaceService = TypeMoq.Mock.ofType(); documentManager = TypeMoq.Mock.ofType(); applicationShell = TypeMoq.Mock.ofType(); const envVariablesProvider = TypeMoq.Mock.ofType(); processService = TypeMoq.Mock.ofType(); - configService = TypeMoq.Mock.ofType(); - const pythonSettings = TypeMoq.Mock.ofType(); - pythonSettings.setup(p => p.pythonPath).returns(() => PYTHON_PATH); - // tslint:disable-next-line:no-any + interpreterService = TypeMoq.Mock.ofType(); + activeResourceService = TypeMoq.Mock.ofType(); + pythonSettings = TypeMoq.Mock.ofType(); + const resource = Uri.parse('a'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any processService.setup((x: any) => x.then).returns(() => undefined); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - envVariablesProvider.setup(e => e.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(workingPython)); const processServiceFactory = TypeMoq.Mock.ofType(); - processServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())).returns(() => processServiceFactory.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager), TypeMoq.It.isAny())).returns(() => documentManager.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())).returns(() => applicationShell.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider), TypeMoq.It.isAny())).returns(() => envVariablesProvider.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => configService.object); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + envVariablesProvider + .setup((e) => e.getEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve({})); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())) + .returns(() => processServiceFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService), TypeMoq.It.isAny())) + .returns(() => interpreterService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager), TypeMoq.It.isAny())) + .returns(() => documentManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) + .returns(() => applicationShell.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(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(); editor = TypeMoq.Mock.ofType(); - editor.setup(e => e.document).returns(() => document.object); + editor.setup((e) => e.document).returns(() => document.object); }); - async function ensureBlankLinesAreRemoved(source: string, expectedSource: string) { - const actualProcessService = new ProcessService(new BufferDecoder()); - processService.setup(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns((file, args, options) => { - return actualProcessService.exec.apply(actualProcessService, [file, args, options]); - }); - const normalizedZCode = await helper.normalizeLines(source); - // In case file has been saved with different line endings. - expectedSource = expectedSource.splitLines({ removeEmptyEntries: false, trim: false }).join(EOL); - expect(normalizedZCode).to.be.equal(expectedSource); - } - test('Ensure blank lines are NOT removed when code is not indented (simple)', async function () { - // This test has not been working for many months in Python 2.7 under - // Windows.Tracked by #2544. - if (isOs(OSType.Windows) && await isPythonVersion('2.7')) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - - const code = ['import sys', '', '', '', 'print(sys.executable)', '', 'print("1234")', '', '', 'print(1)', 'print(2)']; - const expectedCode = code.filter(line => line.trim().length > 0).join(EOL); - await ensureBlankLinesAreRemoved(code.join(EOL), expectedCode); + test('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.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()); - 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]); + + 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.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; }); - 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'].forEach(fileNameSuffix => { - test(`Ensure blank lines are removed (Sample${fileNameSuffix})`, async function () { - // This test has not been working for many months in Python 2.7 under - // Windows.Tracked by #2544. - if (isOs(OSType.Windows) && await isPythonVersion('2.7')) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - - const code = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_raw.py`), 'utf8'); - const expectedCode = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized.py`), 'utf8'); - await ensureBlankLinesAreRemoved(code, expectedCode); - }); - test(`Ensure last two blank lines are preserved (Sample${fileNameSuffix})`, async function () { - // This test has not been working for many months in Python 2.7 under - // Windows.Tracked by #2544. - if (isOs(OSType.Windows) && await isPythonVersion('2.7')) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - - const code = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_raw.py`), 'utf8'); - const expectedCode = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized.py`), 'utf8'); - await ensureBlankLinesAreRemoved(code + EOL, expectedCode + EOL); - }); - test(`Ensure last two blank lines are preserved even if we have more than 2 trailing blank lines (Sample${fileNameSuffix})`, async function () { - // This test has not been working for many months in Python 2.7 under - // Windows.Tracked by #2544. - if (isOs(OSType.Windows) && await isPythonVersion('2.7')) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - - const code = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_raw.py`), 'utf8'); - const expectedCode = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized.py`), 'utf8'); - await ensureBlankLinesAreRemoved(code + EOL + EOL + EOL + EOL, expectedCode + EOL); + + 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]), + ); + 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); + } + + test("Display message if there's no active file", async () => { + documentManager.setup((doc) => doc.activeTextEditor).returns(() => undefined); const uri = await helper.getFileToExecute(); expect(uri).to.be.an('undefined'); - applicationShell.verify(a => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + applicationShell.verify((a) => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); }); test('Display message if active file is unsaved', async () => { - documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); - document.setup(doc => doc.isUntitled).returns(() => true); + documentManager.setup((doc) => doc.activeTextEditor).returns(() => editor.object); + document.setup((doc) => doc.isUntitled).returns(() => true); const uri = await helper.getFileToExecute(); expect(uri).to.be.an('undefined'); - applicationShell.verify(a => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + applicationShell.verify((a) => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); }); test('Display message if active file is non-python', async () => { - document.setup(doc => doc.isUntitled).returns(() => false); - document.setup(doc => doc.languageId).returns(() => 'html'); - documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); + document.setup((doc) => doc.isUntitled).returns(() => false); + document.setup((doc) => doc.languageId).returns(() => 'html'); + documentManager.setup((doc) => doc.activeTextEditor).returns(() => editor.object); const uri = await helper.getFileToExecute(); expect(uri).to.be.an('undefined'); - applicationShell.verify(a => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + applicationShell.verify((a) => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); }); test('Returns file uri', async () => { - document.setup(doc => doc.isUntitled).returns(() => false); - document.setup(doc => doc.languageId).returns(() => PYTHON_LANGUAGE); + document.setup((doc) => doc.isUntitled).returns(() => false); + document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE); const expectedUri = Uri.file('one.py'); - document.setup(doc => doc.uri).returns(() => expectedUri); - documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); + document.setup((doc) => doc.uri).returns(() => expectedUri); + documentManager.setup((doc) => doc.activeTextEditor).returns(() => editor.object); const uri = await helper.getFileToExecute(); expect(uri).to.be.deep.equal(expectedUri); }); test('Returns file uri even if saving fails', async () => { - document.setup(doc => doc.isUntitled).returns(() => false); - document.setup(doc => doc.isDirty).returns(() => true); - document.setup(doc => doc.languageId).returns(() => PYTHON_LANGUAGE); - document.setup(doc => doc.save()).returns(() => Promise.resolve(false)); + document.setup((doc) => doc.isUntitled).returns(() => false); + document.setup((doc) => doc.isDirty).returns(() => true); + document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE); + document.setup((doc) => doc.save()).returns(() => Promise.resolve(false)); const expectedUri = Uri.file('one.py'); - document.setup(doc => doc.uri).returns(() => expectedUri); - documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); + document.setup((doc) => doc.uri).returns(() => expectedUri); + documentManager.setup((doc) => doc.activeTextEditor).returns(() => editor.object); const uri = await helper.getFileToExecute(); expect(uri).to.be.deep.equal(expectedUri); }); test('Dirty files are saved', async () => { - document.setup(doc => doc.isUntitled).returns(() => false); - document.setup(doc => doc.isDirty).returns(() => true); - document.setup(doc => doc.languageId).returns(() => PYTHON_LANGUAGE); + document.setup((doc) => doc.isUntitled).returns(() => false); + document.setup((doc) => doc.isDirty).returns(() => true); + document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE); const expectedUri = Uri.file('one.py'); - document.setup(doc => doc.uri).returns(() => expectedUri); - documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); + document.setup((doc) => doc.uri).returns(() => expectedUri); + documentManager.setup((doc) => doc.activeTextEditor).returns(() => editor.object); const uri = await helper.getFileToExecute(); expect(uri).to.be.deep.equal(expectedUri); - document.verify(doc => doc.save(), TypeMoq.Times.once()); + document.verify((doc) => doc.save(), TypeMoq.Times.once()); }); test('Non-Dirty files are not-saved', async () => { - document.setup(doc => doc.isUntitled).returns(() => false); - document.setup(doc => doc.isDirty).returns(() => false); - document.setup(doc => doc.languageId).returns(() => PYTHON_LANGUAGE); + document.setup((doc) => doc.isUntitled).returns(() => false); + document.setup((doc) => doc.isDirty).returns(() => false); + document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE); const expectedUri = Uri.file('one.py'); - document.setup(doc => doc.uri).returns(() => expectedUri); - documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); + document.setup((doc) => doc.uri).returns(() => expectedUri); + documentManager.setup((doc) => doc.activeTextEditor).returns(() => editor.object); const uri = await helper.getFileToExecute(); expect(uri).to.be.deep.equal(expectedUri); - document.verify(doc => doc.save(), TypeMoq.Times.never()); + document.verify((doc) => doc.save(), TypeMoq.Times.never()); }); - test('Returns current line if nothing is selected', async () => { - const lineContents = 'Line Contents'; - editor.setup(e => e.selection).returns(() => new Selection(3, 0, 3, 0)); + 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.setup(t => t.text).returns(() => lineContents); - document.setup(d => d.lineAt(TypeMoq.It.isAny())).returns(() => textLine.object); + textLine.setup((t) => t.text).returns(() => lineContents); + document.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns(() => textLine.object); const content = await helper.getSelectedTextToExecute(editor.object); expect(content).to.be.equal(lineContents); }); - test('Returns selected text', async () => { - const lineContents = 'Line Contents'; - editor.setup(e => e.selection).returns(() => new Selection(3, 0, 10, 5)); + 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.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.setup(t => t.text).returns(() => lineContents); - document.setup(d => d.getText(TypeMoq.It.isAny())).returns((r: Range) => `${r.start.line}.${r.start.character}.${r.end.line}.${r.end.character}`); + 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('3.0.10.5'); + 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.setup((t) => t.text).returns(() => 'def calc(m, n):'); + const textLine2 = TypeMoq.Mock.ofType(); + textLine2.setup((t) => t.text).returns(() => ' print(m)'); + const textLine3 = TypeMoq.Mock.ofType(); + 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.setup((t) => t.text).returns(() => 'def calc(m, n):'); + const textLine2 = TypeMoq.Mock.ofType(); + textLine2.setup((t) => t.text).returns(() => ' if (m == 0):'); + const textLine3 = TypeMoq.Mock.ofType(); + 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(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.setup((t) => t.text).returns(() => 'def calc(m, n):'); + const textLine2 = TypeMoq.Mock.ofType(); + textLine2.setup((t) => t.text).returns(() => ' if (m > 0'); + const textLine3 = TypeMoq.Mock.ofType(); + 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(expected); }); test('saveFileIfDirty will not fail if file is not opened', async () => { - documentManager.setup(d => d.textDocuments).returns(() => []).verifiable(TypeMoq.Times.once()); + documentManager + .setup((d) => d.textDocuments) + .returns(() => []) + .verifiable(TypeMoq.Times.once()); await helper.saveFileIfDirty(Uri.file(`${__filename}.py`)); documentManager.verifyAll(); }); test('File will be saved if file is dirty', async () => { - documentManager.setup(d => d.textDocuments).returns(() => [document.object]).verifiable(TypeMoq.Times.once()); - document.setup(doc => doc.isUntitled).returns(() => false); - document.setup(doc => doc.isDirty).returns(() => true); - document.setup(doc => doc.languageId).returns(() => PYTHON_LANGUAGE); - const expectedUri = Uri.file('one.py'); - document.setup(doc => doc.uri).returns(() => expectedUri); - - await helper.saveFileIfDirty(expectedUri); - documentManager.verifyAll(); - document.verify(doc => doc.save(), TypeMoq.Times.once()); + documentManager + .setup((d) => d.textDocuments) + .returns(() => [document.object]) + .verifiable(TypeMoq.Times.once()); + document.setup((doc) => doc.isUntitled).returns(() => true); + document.setup((doc) => doc.isDirty).returns(() => true); + document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE); + 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)); + + 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 () => { - documentManager.setup(d => d.textDocuments).returns(() => [document.object]).verifiable(TypeMoq.Times.once()); - document.setup(doc => doc.isUntitled).returns(() => false); - document.setup(doc => doc.isDirty).returns(() => false); - document.setup(doc => doc.languageId).returns(() => PYTHON_LANGUAGE); + documentManager + .setup((d) => d.textDocuments) + .returns(() => [document.object]) + .verifiable(TypeMoq.Times.once()); + document.setup((doc) => doc.isUntitled).returns(() => false); + document.setup((doc) => doc.isDirty).returns(() => false); + document.setup((doc) => doc.languageId).returns(() => PYTHON_LANGUAGE); const expectedUri = Uri.file('one.py'); - document.setup(doc => doc.uri).returns(() => expectedUri); + document.setup((doc) => doc.uri).returns(() => expectedUri); await helper.saveFileIfDirty(expectedUri); documentManager.verifyAll(); - document.verify(doc => doc.save(), TypeMoq.Times.never()); + document.verify((doc) => doc.save(), TypeMoq.Times.never()); }); }); 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; + let applicationShell: TypeMoq.IMock; + + let interpreterService: TypeMoq.IMock; + let commandManager: TypeMoq.IMock; + + let processServiceFactory: TypeMoq.IMock; + let configurationService: TypeMoq.IMock; + + let serviceContainer: TypeMoq.IMock; + let codeExecutionHelper: ICodeExecutionHelper; + let experimentService: TypeMoq.IMock; + + let processService: TypeMoq.IMock; + let activeResourceService: TypeMoq.IMock; + + let document: TypeMoq.IMock; + let pythonSettings: TypeMoq.IMock; + + 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(); + applicationShell = TypeMoq.Mock.ofType(); + processServiceFactory = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + commandManager = TypeMoq.Mock.ofType(); + configurationService = TypeMoq.Mock.ofType(); + serviceContainer = TypeMoq.Mock.ofType(); + experimentService = TypeMoq.Mock.ofType(); + processService = TypeMoq.Mock.ofType(); + activeResourceService = TypeMoq.Mock.ofType(); + pythonSettings = TypeMoq.Mock.ofType(); + 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(); + }); + + test('Cursor is not moved when explicit selection is present', async () => { + const activeEditor = TypeMoq.Mock.ofType(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + 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(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + 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(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + 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(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType(); + 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 0e703bdb1c71..b5bcecd971ea 100644 --- a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts +++ b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts @@ -1,25 +1,41 @@ // 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 { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/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, + 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 => { + ['Terminal Execution', 'Repl Execution', 'Django Execution'].forEach((testSuiteName) => { let terminalSettings: TypeMoq.IMock; let terminalService: TypeMoq.IMock; let workspace: TypeMoq.IMock; @@ -33,15 +49,18 @@ suite('Terminal - Code Execution', () => { let documentManager: TypeMoq.IMock; let commandManager: TypeMoq.IMock; let fileSystem: TypeMoq.IMock; + let pythonExecutionFactory: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; let isDjangoRepl: boolean; + let applicationShell: TypeMoq.IMock; teardown(() => { - disposables.forEach(disposable => { + disposables.forEach((disposable) => { if (disposable) { disposable.dispose(); } }); - + sinon.restore(); disposables = []; }); @@ -56,28 +75,62 @@ suite('Terminal - Code Execution', () => { documentManager = TypeMoq.Mock.ofType(); commandManager = TypeMoq.Mock.ofType(); fileSystem = TypeMoq.Mock.ofType(); - + pythonExecutionFactory = TypeMoq.Mock.ofType(); + interpreterService = TypeMoq.Mock.ofType(); + applicationShell = TypeMoq.Mock.ofType(); settings = TypeMoq.Mock.ofType(); - settings.setup(s => s.terminal).returns(() => terminalSettings.object); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + settings.setup((s) => s.terminal).returns(() => terminalSettings.object); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); switch (testSuiteName) { case 'Terminal Execution': { - executor = new TerminalCodeExecutionProvider(terminalFactory.object, configService.object, workspace.object, disposables, platform.object); + executor = new TerminalCodeExecutionProvider( + terminalFactory.object, + configService.object, + workspace.object, + disposables, + platform.object, + interpreterService.object, + commandManager.object, + applicationShell.object, + ); break; } case 'Repl Execution': { - executor = new ReplProvider(terminalFactory.object, configService.object, workspace.object, disposables, platform.object); + executor = new ReplProvider( + terminalFactory.object, + configService.object, + workspace.object, + disposables, + platform.object, + interpreterService.object, + commandManager.object, + applicationShell.object, + ); expectedTerminalTitle = 'REPL'; break; } case 'Django Execution': { isDjangoRepl = true; - workspace.setup(w => w.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { - return { dispose: noop }; - }); - executor = new DjangoShellCodeExecutionProvider(terminalFactory.object, configService.object, workspace.object, documentManager.object, - platform.object, commandManager.object, fileSystem.object, disposables); + workspace + .setup((w) => + w.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + ) + .returns(() => { + return { dispose: noop }; + }); + executor = new DjangoShellCodeExecutionProvider( + terminalFactory.object, + configService.object, + workspace.object, + documentManager.object, + platform.object, + commandManager.object, + fileSystem.object, + disposables, + interpreterService.object, + applicationShell.object, + ); expectedTerminalTitle = 'Django Shell'; break; } @@ -89,15 +142,27 @@ suite('Terminal - Code Execution', () => { suite(`${testSuiteName} (validation of title)`, () => { setup(() => { - terminalFactory.setup(f => f.getTerminalService(TypeMoq.It.isAny(), TypeMoq.It.isValue(expectedTerminalTitle))).returns(() => terminalService.object); + terminalFactory + .setup((f) => + f.getTerminalService( + TypeMoq.It.is((a) => a.title === expectedTerminalTitle), + ), + ) + .returns(() => terminalService.object); }); - async function ensureTerminalIsCreatedUponInvokingInitializeRepl(isWindows: boolean, isOsx: boolean, isLinux: boolean): Promise { - platform.setup(p => p.isWindows).returns(() => isWindows); - platform.setup(p => p.isMac).returns(() => isOsx); - platform.setup(p => p.isLinux).returns(() => isLinux); - settings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); - terminalSettings.setup(t => t.launchArgs).returns(() => []); + async function ensureTerminalIsCreatedUponInvokingInitializeRepl( + isWindows: boolean, + isOsx: boolean, + isLinux: boolean, + ): Promise { + platform.setup((p) => p.isWindows).returns(() => isWindows); + platform.setup((p) => p.isMac).returns(() => isOsx); + platform.setup((p) => p.isLinux).returns(() => isLinux); + 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(); } @@ -116,24 +181,78 @@ 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())).returns(() => terminalService.object); + terminalFactory + .setup((f) => f.getTerminalService(TypeMoq.It.isAny())) + .returns(() => terminalService.object); + }); + + async function ensureWeSetCurrentDriveBeforeChangingDirectory(_isWindows: boolean): Promise { + 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 ensureWeSetCurrentDirectoryBeforeExecutingAFile(isWindows: boolean): Promise { + async function ensureWeDoNotChangeDriveIfDriveLetterSameAsFileDriveLetter( + _isWindows: boolean, + ): Promise { + 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 { const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); - terminalSettings.setup(t => t.executeInFileDir).returns(() => true); - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); - workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); - platform.setup(p => p.isWindows).returns(() => false); - settings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); - terminalSettings.setup(t => t.launchArgs).returns(() => []); + 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); + 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()); + terminalService.verify( + async (t) => + 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 () => { await ensureWeSetCurrentDirectoryBeforeExecutingAFile(false); @@ -144,16 +263,18 @@ suite('Terminal - Code Execution', () => { async function ensureWeWetCurrentDirectoryAndQuoteBeforeExecutingFile(isWindows: boolean): Promise { const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); - terminalSettings.setup(t => t.executeInFileDir).returns(() => true); - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); - workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); - platform.setup(p => p.isWindows).returns(() => isWindows); - settings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); - terminalSettings.setup(t => t.launchArgs).returns(() => []); + terminalSettings.setup((t) => t.executeInFileDir).returns(() => true); + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); + workspaceFolder.setup((w) => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); + platform.setup((p) => p.isWindows).returns(() => isWindows); + 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(); - terminalService.verify(async t => t.sendText(TypeMoq.It.isValue(`cd ${dir}`)), TypeMoq.Times.once()); + const dir = path.dirname(file.fsPath).fileToCommandArgumentForPythonExt(); + terminalService.verify(async (t) => t.sendText(TypeMoq.It.isValue(`cd ${dir}`)), TypeMoq.Times.once()); } test('Ensure we set current directory (and quote it when containing spaces) before executing file (non windows)', async () => { @@ -164,56 +285,80 @@ suite('Terminal - Code Execution', () => { await ensureWeWetCurrentDirectoryAndQuoteBeforeExecutingFile(true); }); - async function ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileInSameDirectory(isWindows: boolean): Promise { + async function ensureWeSetCurrentDirectoryBeforeExecutingFileInWorkspaceDirectory( + isWindows: boolean, + ): Promise { const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); - terminalSettings.setup(t => t.executeInFileDir).returns(() => true); - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); - workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to', 'file with spaces in path'))); - platform.setup(p => p.isWindows).returns(() => isWindows); - settings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); - terminalSettings.setup(t => t.launchArgs).returns(() => []); + terminalSettings.setup((t) => t.executeInFileDir).returns(() => true); + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); + workspaceFolder + .setup((w) => w.uri) + .returns(() => Uri.file(path.join('c', 'path', 'to', 'file with spaces in path'))); + platform.setup((p) => p.isWindows).returns(() => isWindows); + 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 ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory(isWindows: boolean): Promise { + async function ensureWeSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory( + isWindows: boolean, + ): Promise { const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); - terminalSettings.setup(t => t.executeInFileDir).returns(() => true); - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); - platform.setup(p => p.isWindows).returns(() => isWindows); - settings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); - terminalSettings.setup(t => t.launchArgs).returns(() => []); + terminalSettings.setup((t) => t.executeInFileDir).returns(() => true); + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + platform.setup((p) => p.isWindows).returns(() => isWindows); + 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 file is not in a workspace (non windows)', async () => { - await ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory(false); + test('Ensure we set current directory before executing file if file is not in a workspace (non windows)', async () => { + await ensureWeSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory(false); }); - test('Ensure we do not set current directory before executing file if file is not in a workspace (windows)', async () => { - await ensureWeDoNotSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory(true); + test('Ensure we set current directory before executing file if file is not in a workspace (windows)', async () => { + await ensureWeSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory(true); }); - async function testFileExecution(isWindows: boolean, pythonPath: string, terminalArgs: string[], file: Uri): Promise { - platform.setup(p => p.isWindows).returns(() => isWindows); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); - terminalSettings.setup(t => t.executeInFileDir).returns(() => false); - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + async function testFileExecution( + isWindows: boolean, + pythonPath: string, + terminalArgs: string[], + file: Uri, + ): Promise { + platform.setup((p) => p.isWindows).returns(() => isWindows); + 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())) + .returns(() => Promise.resolve(undefined)); await executor.executeFile(file); const expectedPythonPath = isWindows ? pythonPath.replace(/\\/g, '/') : pythonPath; - const expectedArgs = terminalArgs.concat(file.fsPath.fileToCommandArgument()); - terminalService.verify(async t => t.sendCommand(TypeMoq.It.isValue(expectedPythonPath), TypeMoq.It.isValue(expectedArgs)), TypeMoq.Times.once()); + const expectedArgs = terminalArgs.concat(file.fsPath.fileToCommandArgumentForPythonExt()); + terminalService.verify( + async (t) => + t.sendCommand(TypeMoq.It.isValue(expectedPythonPath), TypeMoq.It.isValue(expectedArgs)), + TypeMoq.Times.once(), + ); } test('Ensure python file execution script is sent to terminal on windows', async () => { @@ -236,124 +381,297 @@ suite('Terminal - Code Execution', () => { await testFileExecution(false, PYTHON_PATH, ['-a', '-b', '-c'], file); }); - function testReplCommandArguments(isWindows: boolean, pythonPath: string, expectedPythonPath: string, terminalArgs: string[]) { - platform.setup(p => p.isWindows).returns(() => isWindows); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + async function testCondaFileExecution( + pythonPath: string, + terminalArgs: string[], + file: Uri, + condaEnv: { name: string; path: string }, + ): Promise { + 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(); + 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, + execForLinter: procs.execForLinter, + }; + pythonExecutionFactory + .setup((p) => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(condaExecutionService)); + + await executor.executeFile(file); + + const expectedArgs = [...terminalArgs, file.fsPath.fileToCommandArgumentForPythonExt()]; + + terminalService.verify( + async (t) => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedArgs)), + TypeMoq.Times.once(), + ); + } + + test('Ensure conda args with conda env name are sent to terminal if there is a conda environment with a name', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + await testCondaFileExecution(PYTHON_PATH, ['-a', '-b', '-c'], file, { + name: 'foo-env', + path: 'path/to/foo-env', + }); + }); + + test('Ensure conda args with conda env path are sent to terminal if there is a conda environment without a name', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + await testCondaFileExecution(PYTHON_PATH, ['-a', '-b', '-c'], file, { + name: '', + path: 'path/to/foo-env', + }); + }); + + async function testReplCommandArguments( + isWindows: boolean, + pythonPath: string, + expectedPythonPath: string, + terminalArgs: string[], + ) { + pythonExecutionFactory + .setup((p) => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + platform.setup((p) => p.isWindows).returns(() => isWindows); + 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; - const replCommandArgs = (executor as TerminalCodeExecutionProvider).getReplCommandArgs(); + const replCommandArgs = await (executor as TerminalCodeExecutionProvider).getExecutableInfo(); expect(replCommandArgs).not.to.be.an('undefined', 'Command args is undefined'); expect(replCommandArgs.command).to.be.equal(expectedPythonPath, 'Incorrect python path'); expect(replCommandArgs.args).to.be.deep.equal(expectedTerminalArgs, 'Incorrect arguments'); } - test('Ensure fully qualified python path is escaped when building repl args on Windows', () => { + test('Ensure fully qualified python path is escaped when building repl args on Windows', async () => { const pythonPath = 'c:\\program files\\python\\python.exe'; const terminalArgs = ['-a', 'b', 'c']; - testReplCommandArguments(true, pythonPath, 'c:/program files/python/python.exe', terminalArgs); + await testReplCommandArguments(true, pythonPath, 'c:/program files/python/python.exe', terminalArgs); }); - test('Ensure fully qualified python path is returned as is, when building repl args on Windows', () => { + test('Ensure fully qualified python path is returned as is, when building repl args on Windows', async () => { const pythonPath = 'c:/program files/python/python.exe'; const terminalArgs = ['-a', 'b', 'c']; - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); }); - test('Ensure python path is returned as is, when building repl args on Windows', () => { + test('Ensure python path is returned as is, when building repl args on Windows', async () => { const pythonPath = PYTHON_PATH; const terminalArgs = ['-a', 'b', 'c']; - testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); + await testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); }); - test('Ensure fully qualified python path is returned as is, on non Windows', () => { + test('Ensure fully qualified python path is returned as is, on non Windows', async () => { const pythonPath = 'usr/bin/python'; const terminalArgs = ['-a', 'b', 'c']; - testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); + await testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); }); - test('Ensure python path is returned as is, on non Windows', () => { + test('Ensure python path is returned as is, on non Windows', async () => { const pythonPath = PYTHON_PATH; const terminalArgs = ['-a', 'b', 'c']; - testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); + await testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); + }); + + async function testReplCondaCommandArguments( + pythonPath: string, + terminalArgs: string[], + condaEnv: { name: string; path: string }, + ) { + 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(); + 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, + execForLinter: procs.execForLinter, + }; + pythonExecutionFactory + .setup((p) => p.createCondaExecutionService(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(condaExecutionService)); + + const djangoArgs = isDjangoRepl ? ['manage.py', 'shell'] : []; + const expectedTerminalArgs = [...terminalArgs, ...djangoArgs]; + + const replCommandArgs = await (executor as TerminalCodeExecutionProvider).getExecutableInfo(); + + expect(replCommandArgs).not.to.be.an('undefined', 'Conda command args are undefined'); + expect(replCommandArgs.command).to.be.equal(pythonPath, 'Repl needs to use python, not conda'); + expect(replCommandArgs.args).to.be.deep.equal(expectedTerminalArgs, 'Incorrect terminal arguments'); + } + + test('Ensure conda args with env name are returned when building repl args with a conda env with a name', async () => { + await testReplCondaCommandArguments(PYTHON_PATH, ['-a', 'b', 'c'], { + name: 'foo-env', + path: 'path/to/foo-env', + }); + }); + + test('Ensure conda args with env path are returned when building repl args with a conda env without a name', async () => { + await testReplCondaCommandArguments(PYTHON_PATH, ['-a', 'b', 'c'], { + name: '', + path: 'path/to/foo-env', + }); }); test('Ensure nothing happens when blank text is sent to the terminal', async () => { await executor.execute(''); await executor.execute(' '); - // tslint:disable-next-line:no-any - await executor.execute(undefined as any as string); - terminalService.verify(async t => t.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - terminalService.verify(async t => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); + await executor.execute((undefined as any) as string); + + terminalService.verify( + async (t) => t.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never(), + ); + terminalService.verify(async (t) => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); }); test('Ensure repl is initialized once before sending text to the repl', async () => { const pythonPath = 'usr/bin/python1234'; const terminalArgs = ['-a', 'b', 'c']; - platform.setup(p => p.isWindows).returns(() => false); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + 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'); await executor.execute('cmd2'); await executor.execute('cmd3'); const expectedTerminalArgs = isDjangoRepl ? terminalArgs.concat(['manage.py', 'shell']) : terminalArgs; - terminalService.verify(async t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), TypeMoq.Times.once()); + terminalService.verify( + async (t) => + t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(expectedTerminalArgs)), + TypeMoq.Times.once(), + ); }); - test('Ensure repl is re-initialized when terminal is closed', async () => { + 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); - 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 - }; - })); + 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'); 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(), + ); }); test('Ensure code is sent to terminal', async () => { const pythonPath = 'usr/bin/python1234'; const terminalArgs = ['-a', 'b', 'c']; - platform.setup(p => p.isWindows).returns(() => false); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + 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'); - 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 new file mode 100644 index 000000000000..4f865cdedc0d --- /dev/null +++ b/src/test/terminals/serviceRegistry.unit.test.ts @@ -0,0 +1,79 @@ +/* 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 { IExtensionActivationService, IExtensionSingleActivationService } from '../../client/activation/types'; +import { IServiceManager } from '../../client/ioc/types'; +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, + 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', () => { + const services = typemoq.Mock.ofType(undefined, typemoq.MockBehavior.Strict); + [ + [ICodeExecutionHelper, CodeExecutionHelper], + [ICodeExecutionManager, CodeExecutionManager], + [ICodeExecutionService, DjangoShellCodeExecutionProvider, 'djangoShell'], + [ICodeExecutionService, ReplProvider, 'repl'], + [ITerminalAutoActivation, TerminalAutoActivation], + [ICodeExecutionService, TerminalCodeExecutionProvider, 'standard'], + [ITerminalEnvVarCollectionService, TerminalEnvVarCollectionService], + [IExtensionSingleActivationService, TerminalIndicatorPrompt], + [ITerminalDeactivateService, TerminalDeactivateService], + [IShellIntegrationDetectionService, ShellIntegrationDetectionService], + ].forEach((args) => { + if (args.length === 2) { + services + .setup((s) => + s.addSingleton( + 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( + 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); + + services.verifyAll(); + }); +}); 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; + let editorConfig: TypeMoq.IMock; + let context: TypeMoq.IMock; + let createDirectoryStub: sinon.SinonStub; + let copyStub: sinon.SinonStub; + let globalEnvironmentVariableCollection: TypeMoq.IMock; + + setup(() => { + context = TypeMoq.Mock.ofType(); + globalEnvironmentVariableCollection = TypeMoq.Mock.ofType(); + 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(); + editorConfig = TypeMoq.Mock.ofType(); + 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().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().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().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 new file mode 100644 index 000000000000..ab902255203b --- /dev/null +++ b/src/test/testBootstrap.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ChildProcess, spawn, SpawnOptions } from 'child_process'; +import * as fs from '../client/common/platform/fs-paths'; +import { AddressInfo, createServer, Server } from 'net'; +import * as path from 'path'; +import { EXTENSION_ROOT_DIR } from '../client/constants'; +import { noop, sleep } from './core'; +import { initializeLogger } from './testLogger'; + +initializeLogger(); + +/* +This is a simple work around for tests tasks not completing on Azure Pipelines. +What's been happening is, the tests run however for some readon the Node propcess (VS Code) does not exit. +Here's what we've tried thus far: +* Dispose all timers +* Close all open streams/sockets. +* Use `process.exit` and use the VSC commands to close itself. + +Final solution: +* Start a node.js procecss + * This process will start a socket server + * This procecss will start the tests in a separate procecss (spawn) +* When the tests have completed, + * Send a message to the socket server with a flag (true/false whether tests passed/failed) +* Socket server (main procecss) will receive the test status flag. + * This will kill the spawned process + * This main process will kill itself with exit code 0 if tests pass succesfully, else 1. +*/ + +const testFile = process.argv[2]; +const portFile = path.join(EXTENSION_ROOT_DIR, 'port.txt'); + +let proc: ChildProcess | undefined; +let server: Server | undefined; + +async function deletePortFile() { + try { + if (await fs.pathExists(portFile)) { + await fs.unlink(portFile); + } + } catch { + noop(); + } +} +async function end(exitCode: number) { + if (exitCode === 0) { + console.log('Exiting without errors'); + } else { + console.error('Exiting with test failures'); + } + if (proc) { + try { + const procToKill = proc; + proc = undefined; + console.log('Killing VSC'); + await deletePortFile(); + // Wait for the std buffers to get flushed before killing. + await sleep(5_000); + procToKill.kill(); + } catch { + noop(); + } + } + if (server) { + server.close(); + } + // Exit with required code. + process.exit(exitCode); +} + +async function startSocketServer() { + return new Promise((resolve) => { + server = createServer((socket) => { + socket.on('data', (buffer) => { + const data = buffer.toString('utf8'); + console.log(`Exit code from Tests is ${data}`); + const code = parseInt(data.substring(0, 1), 10); + end(code).catch(noop); + }); + socket.on('error', (ex) => { + // Just log it, no need to do anything else. + console.error(ex); + }); + }); + + server.listen( + { host: '127.0.0.1', port: 0 }, + async (): Promise => { + const port = (server!.address() as AddressInfo).port; + console.log(`Test server listening on port ${port}`); + await deletePortFile(); + await fs.writeFile(portFile, port.toString()); + resolve(); + }, + ); + server.on('error', (ex) => { + // Just log it, no need to do anything else. + console.error(ex); + }); + }); +} + +async function start() { + await startSocketServer(); + const options: SpawnOptions = { cwd: process.cwd(), env: process.env, detached: true, stdio: 'inherit' }; + proc = spawn(process.execPath, [testFile], options); + proc.once('close', end); +} + +start().catch((ex) => { + console.error('File testBootstrap.ts failed with Errors', ex); + process.exit(1); +}); diff --git a/src/test/testLogger.ts b/src/test/testLogger.ts new file mode 100644 index 000000000000..26484ee119c7 --- /dev/null +++ b/src/test/testLogger.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'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. + +const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; + +export function initializeLogger() { + if (isCI && process.env.VSC_PYTHON_LOG_FILE) { + initializeFileLogging([]); + // Send console.*() to the non-console loggers. + monkeypatchConsole(); + } +} + +/** + * What we're doing here is monkey patching the console.log so we can + * send everything sent to console window into our logs. This is only + * required when we're directly writing to `console.log` or not using + * our `winston logger`. This is something we'd generally turn on only + * on CI so we can see everything logged to the console window + * (via the logs). + */ +function monkeypatchConsole() { + // The logging "streams" (methods) of the node console. + const streams = ['log', 'error', 'warn', 'info', 'debug', 'trace']; + const levels: { [key: string]: LogLevel } = { + error: LogLevel.Error, + warn: LogLevel.Warning, + debug: LogLevel.Debug, + trace: LogLevel.Debug, + info: LogLevel.Info, + log: LogLevel.Info, + }; + + const consoleAny: any = console; + for (const stream of streams) { + // Using symbols guarantee the properties will be unique & prevents + // clashing with names other code/library may create or have created. + // We could use a closure but it's a bit trickier. + const sym = Symbol.for(stream); + consoleAny[sym] = consoleAny[stream]; + consoleAny[stream] = function () { + const args = Array.prototype.slice.call(arguments); + const fn = consoleAny[sym]; + fn(...args); + const level = levels[stream] || LogLevel.Info; + logTo(level, args); + }; + } +} diff --git a/src/test/testRunner.ts b/src/test/testRunner.ts index 55c2bb60146e..6187597a46a3 100644 --- a/src/test/testRunner.ts +++ b/src/test/testRunner.ts @@ -1,236 +1,94 @@ -// 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 fs from 'fs-extra'; -import * as glob from 'glob'; -import * as istanbul from 'istanbul'; -import * as Mocha from 'mocha'; -import * as path from 'path'; -import { MochaSetupOptions } from 'vscode/lib/testrunner'; -const remapIstanbul = require('remap-istanbul'); -import { setUpDomEnvironment } from './datascience/reactHelpers'; - -interface ITestRunnerOptions { - enabled?: boolean; - relativeCoverageDir: string; - relativeSourcePath: string; - ignorePatterns: string[]; - includePid?: boolean; - reports?: string[]; - verbose?: boolean; -} - -// http://gotwarlost.github.io/istanbul/public/apidocs/files/lib_instrumenter.js.html#l478. -type CoverState = { - path: string; - s: {}; - b: {}; - f: {}; - fnMap: {}; - statementMap: {}; - branchMap: {}; -}; - -type Instrumenter = istanbul.Instrumenter & { coverState: CoverState }; -type TestCallback = (error?: Error, failures?: number) => void; - -// Linux: prevent a weird NPE when mocha on Linux requires the window size from the TTY. -// Since we are not running in a tty environment, we just implement the method statically. -const tty = require('tty'); -if (!tty.getWindowSize) { - tty.getWindowSize = function (): number[] { return [80, 75]; }; -} - -let mocha = new Mocha({ - ui: 'tdd', - useColors: true -}); - -export type SetupOptions = MochaSetupOptions & { - testFilesSuffix?: string; - reporter?: string; - reporterOptions?: { - mochaFile?: string; - properties?: string; - }; -}; - -let testFilesGlob = 'test'; -let coverageOptions: { coverageConfig: string } | undefined; - -export function configure(setupOptions: SetupOptions, coverageOpts?: { coverageConfig: string }): void { - if (setupOptions.testFilesSuffix) { - testFilesGlob = setupOptions.testFilesSuffix; - } - mocha = new Mocha(setupOptions); - coverageOptions = coverageOpts; -} - -export function run(testsRoot: string, callback: TestCallback): 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. - setUpDomEnvironment(); - - // Check whether code coverage is enabled. - const options = getCoverageOptions(testsRoot); - if (options && options.enabled) { - // Setup coverage pre-test, including post-test hook to report. - // tslint:disable-next-line:no-use-before-declare - const coverageRunner = new CoverageRunner(options, testsRoot, callback); - coverageRunner.setupCoverage(); - } - - // Run the tests. - glob(`**/**.${testFilesGlob}.js`, { ignore: ['**/**.unit.test.js', '**/**.functional.test.js'], cwd: testsRoot }, (error, files) => { - if (error) { - return callback(error); - } - try { - files.forEach(file => mocha.addFile(path.join(testsRoot, file))); - mocha.run((failures) => callback(undefined, failures)); - } catch (error) { - return callback(error); - } - }); -} - -function getCoverageOptions(testsRoot: string): ITestRunnerOptions | undefined { - if (!coverageOptions) { - return undefined; - } - const coverConfigPath = path.join(testsRoot, coverageOptions.coverageConfig); - return fs.existsSync(coverConfigPath) ? JSON.parse(fs.readFileSync(coverConfigPath, 'utf8')) : undefined; -} - -class CoverageRunner { - private coverageVar: string = `$$cov_${new Date().getTime()}$$`; - private sourceFiles: string[] = []; - private instrumenter!: Instrumenter; - - private get coverage(): { [key: string]: CoverState } { - if (global[this.coverageVar] === undefined || Object.keys(global[this.coverageVar]).length === 0) { - console.error('No coverage information was collected, exit without writing coverage information'); - return {}; - } else { - return global[this.coverageVar]; - } - } - private set coverage(value: { [key: string]: CoverState }) { - global[this.coverageVar] = value; - } - - constructor(private options: ITestRunnerOptions, private testsRoot: string, endRunCallback: TestCallback) { - if (!options.relativeSourcePath) { - endRunCallback(new Error('Error - relativeSourcePath must be defined for code coverage to work')); - } - } - /** - * Information on hooking up code coverage can be found here: - * http://tannguyen.org/2017/04/gulp-mocha-and-istanbul/ - * http://gotwarlost.github.io/istanbul/public/apidocs/classes/HookOptions.html - * @memberof CoverageRunner - */ - public setupCoverage(): void { - const reportingDir = path.join(this.testsRoot, this.options.relativeCoverageDir); - fs.emptyDirSync(reportingDir); - - // Set up Code Coverage, hooking require so that instrumented code is returned. - this.instrumenter = new istanbul.Instrumenter({ coverageVariable: this.coverageVar }) as Instrumenter; - const sourceRoot = path.join(this.testsRoot, this.options.relativeSourcePath); - - // Glob source files - const srcFiles = glob.sync('**/**.js', { - ignore: this.options.ignorePatterns, - cwd: sourceRoot - }); - - // Create a match function - taken from the run-with-cover.js in istanbul. - const decache = require('decache'); - const fileMap = new Set(); - srcFiles - .map(file => path.join(sourceRoot, file)) - .forEach(fullPath => { - fileMap.add(fullPath); - - // On Windows, extension is loaded pre-test hooks and this mean we lose - // our chance to hook the Require call. In order to instrument the code - // we have to decache the JS file so on next load it gets instrumented. - // This doesn't impact tests, but is a concern if we had some integration - // tests that relied on VSCode accessing our module since there could be - // some shared global state that we lose. - decache(fullPath); - }); - - const matchFn = (file: string) => fileMap.has(file); - this.sourceFiles = Array.from(fileMap.keys()); - - // http://gotwarlost.github.io/istanbul/public/apidocs/classes/Hook.html#method_hookRequire. - // Hook up to the Require function so that when this is called, if any of our source files - // are required, the instrumented version is pulled in instead. These instrumented versions - // write to a global coverage variable with hit counts whenever they are accessed. - const transformer = this.instrumenter.instrumentSync.bind(this.instrumenter); - const hookOpts = { verbose: false, extensions: ['.js'] }; - (istanbul.hook).hookRequire(matchFn, transformer, hookOpts); - - // Initialize the global variable to store instrumentation details. - // http://gotwarlost.github.io/istanbul/public/apidocs/classes/Instrumenter.html. - this.coverage = {}; - - // Hook the process exit event to handle reporting, - // Only report coverage if the process is exiting successfully. - process.on('exit', () => this.reportCoverage()); - } - - /** - * Writes a coverage report. Note that as this is called in the process exit callback, all calls must be synchronous. - * @returns {void} - * @memberOf CoverageRunner - */ - public reportCoverage(): void { - (istanbul.hook).unhookRequire(); - const coverage = this.coverage; - - // Files that are not touched by code ran by the test runner is manually instrumented, to - // illustrate the missing coverage. - this.sourceFiles - .filter(file => !coverage[file]) - .forEach(file => { - this.instrumenter.instrumentSync(fs.readFileSync(file, 'utf-8'), file); - - // When instrumenting the code, istanbul will give each FunctionDeclaration a value of 1 in coverState.s, - // presumably to compensate for function hoisting. We need to reset this, as the function was not hoisted, - // as it was never loaded. - Object.keys(this.instrumenter.coverState.s).forEach(key => this.instrumenter.coverState.s[key] = 0); - - coverage[file] = this.instrumenter.coverState; - }); - - const reportingDir = path.join(this.testsRoot, this.options.relativeCoverageDir); - const coverageFile = path.join(reportingDir, 'coverage.json'); - - fs.mkdirsSync(reportingDir); - fs.writeFileSync(coverageFile, JSON.stringify(coverage), 'utf8'); - - const remappedCollector: istanbul.Collector = remapIstanbul.remap(coverage, { - warn: warning => { - // We expect some warnings as any JS file without a typescript mapping will cause this. - // By default, we'll skip printing these to the console as it clutters it up. - if (this.options.verbose) { - console.warn(warning); - } - } - }); - - const reporter = new istanbul.Reporter(undefined, reportingDir); - const reportTypes = Array.isArray(this.options.reports) ? this.options.reports! : ['lcov']; - reporter.addAll(reportTypes); - reporter.write(remappedCollector, true, () => console.log(`reports written to ${reportingDir}`)); - } -} +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// 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 { MAX_EXTENSION_ACTIVATION_TIME } from './constants'; +import { initialize } from './initialize'; + +// Linux: prevent a weird NPE when mocha on Linux requires the window size from the TTY. +// Since we are not running in a tty environment, we just implement the method statically. +const tty = require('tty'); +if (!tty.getWindowSize) { + tty.getWindowSize = function (): number[] { + return [80, 75]; + }; +} + +let mocha = new Mocha.default({ + ui: 'tdd', + colors: true, +}); + +export type SetupOptions = Mocha.MochaOptions & { + testFilesSuffix?: string; + reporterOptions?: { + mochaFile?: string; + properties?: string; + }; +}; + +let testFilesGlob = 'test'; + +export function configure(setupOptions: SetupOptions): void { + if (setupOptions.testFilesSuffix) { + testFilesGlob = setupOptions.testFilesSuffix; + } + // Force Mocha to exit. + (setupOptions as any).exit = true; + mocha = new Mocha.default(setupOptions); +} + +export async function run(): Promise { + const testsRoot = path.join(__dirname); + // Enable source map support. + require('source-map-support').install(); + + /** + * Waits until the Python Extension completes loading or a timeout. + * When running tests within VSC, we need to wait for the Python Extension to complete loading, + * this is where `initialize` comes in, we load the PVSC extension using VSC API, wait for it + * to complete. + * That's when we know out PVSC extension specific code is ready for testing. + * So, this code needs to run always for every test running in VS Code (what we call these `system test`) . + * @returns + */ + function initializationScript() { + const ex = new Error('Failed to initialize Python extension for tests after 3 minutes'); + let timer: NodeJS.Timeout | undefined; + const failed = new Promise((_, reject) => { + timer = setTimeout(() => reject(ex), MAX_EXTENSION_ACTIVATION_TIME); + }); + const promise = Promise.race([initialize(), failed]); + promise.then(() => clearTimeout(timer!)).catch(() => clearTimeout(timer!)); + return promise; + } + // Run the tests. + await new Promise((resolve, reject) => { + glob.default( + `**/**.${testFilesGlob}.js`, + { ignore: ['**/**.unit.test.js', '**/**.functional.test.js'], cwd: testsRoot }, + (error, files) => { + if (error) { + return reject(error); + } + try { + files.forEach((file) => mocha.addFile(path.join(testsRoot, file))); + initializationScript() + .then(() => + mocha.run((failures) => + failures > 0 ? reject(new Error(`${failures} total failures`)) : resolve(), + ), + ) + .catch(reject); + } catch (error) { + return reject(error); + } + }, + ); + }); +} diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts new file mode 100644 index 000000000000..86e862103bf6 --- /dev/null +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -0,0 +1,933 @@ +// 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 sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +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 } from '../../../client/common/application/types'; +import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; +import '../../../client/common/extensions'; +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 } 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.default); + +suite('Unit Tests - Debug Launcher', () => { + let serviceContainer: TypeMoq.IMock; + let unitTestSettings: TypeMoq.IMock; + let debugLauncher: DebugLauncher; + let debugService: TypeMoq.IMock; + let settings: TypeMoq.IMock; + let debugEnvHelper: TypeMoq.IMock; + let interpreterService: TypeMoq.IMock; + let environmentActivationService: TypeMoq.IMock; + 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(); + environmentActivationService + .setup((e) => e.getActivatedEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(envVars)); + interpreterService = TypeMoq.Mock.ofType(); + serviceContainer = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + const configService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + + debugService = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IDebugService))).returns(() => debugService.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(undefined, TypeMoq.MockBehavior.Strict); + appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + + settings = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + + unitTestSettings = TypeMoq.Mock.ofType(); + settings.setup((p) => p.testing).returns(() => unitTestSettings.object); + + debugEnvHelper = TypeMoq.Mock.ofType(undefined, TypeMoq.MockBehavior.Strict); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDebugEnvironmentVariablesService))) + .returns(() => debugEnvHelper.object); + + debugLauncher = new DebugLauncher(serviceContainer.object, getNewResolver(configService.object)); + }); + + teardown(() => { + sinon.restore(); + }); + + function getNewResolver(configService: IConfigurationService) { + const validator = TypeMoq.Mock.ofType( + undefined, + TypeMoq.MockBehavior.Strict, + ); + validator + .setup((v) => v.validatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)); + return new LaunchConfigurationResolver( + validator.object, + configService, + debugEnvHelper.object, + interpreterService.object, + environmentActivationService.object, + ); + } + function setupDebugManager( + _workspaceFolder: WorkspaceFolder, + expected: DebugConfiguration, + testProvider: TestProvider, + ) { + 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((x) => x.getEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(expected.env)); + + const deferred = createDeferred(); + 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.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_wspc: WorkspaceFolder, config: DebugConfiguration) => { + capturedConfig = config; + deferred.resolve(); + }) + .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), + }; + } + 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: PythonDebuggerTypeName, + request: 'launch', + console: 'internalConsole', + env: {}, + envFile: __filename, + stopOnEntry: false, + showReturnValue: true, + redirectOutput: true, + debugStdLib: false, + subProcess: true, + purpose: [], + }; + } + function setupSuccess( + options: LaunchOptions, + testProvider: TestProvider, + expected?: DebugConfiguration, + debugConfigs?: string | DebugConfiguration[], + ) { + const testLaunchScript = getTestLauncherScript(testProvider, false); + + const workspaceFolders = [createWorkspaceFolder(options.cwd), createWorkspaceFolder('five/six/seven')]; + getWorkspaceFoldersStub.returns(workspaceFolders); + getWorkspaceFolderStub.returns(workspaceFolders[0]); + + if (!debugConfigs) { + pathExistsStub.resolves(false); + } else { + pathExistsStub.resolves(true); + + if (typeof debugConfigs !== 'string') { + debugConfigs = JSON.stringify({ + version: '0.1.0', + configurations: debugConfigs, + }); + } + readFileStub.resolves(debugConfigs as string); + } + + if (!expected) { + expected = getDefaultDebugConfig(); + } + 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.python) { + expected.python = 'python'; + } + if (!expected.clientOS) { + expected.clientOS = isOs(OSType.Windows) ? 'windows' : 'unix'; + } + 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); + } + if (expected.showReturnValue) { + expected.debugOptions.push(DebugOptions.ShowReturnValue); + } + if (expected.redirectOutput) { + expected.debugOptions.push(DebugOptions.RedirectOutput); + } + if (expected.subProcess) { + expected.debugOptions.push(DebugOptions.SubProcess); + } + if (isOs(OSType.Windows)) { + expected.debugOptions.push(DebugOptions.FixFilePathCase); + } + + setupDebugManager(workspaceFolders[0], expected, testProvider); + } + + const testProviders: TestProvider[] = ['pytest', 'unittest']; + + testProviders.forEach((testProvider) => { + const testTitleSuffix = `(Test Framework '${testProvider}')`; + + test(`Must launch debugger ${testTitleSuffix}`, async () => { + const options = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + setupSuccess(options, testProvider); + + await debugLauncher.launchDebugger(options); + + 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, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + setupSuccess(options, testProvider); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + test(`Must not launch debugger if cancelled ${testTitleSuffix}`, async () => { + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + return Promise.resolve(undefined as any); + }) + .verifiable(TypeMoq.Times.never()); + + const cancellationToken = new CancellationTokenSource(); + cancellationToken.cancel(); + const token = cancellationToken.token; + const options: LaunchOptions = { + cwd: '', + args: [], + token, + testProvider, + 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 () => { + getWorkspaceFoldersStub.returns(undefined); + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + console.log('Debugging should not start'); + return Promise.resolve(undefined as any); + }) + .verifiable(TypeMoq.Times.never()); + + const options: LaunchOptions = { + cwd: '', + args: [], + testProvider, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + await expect(debugLauncher.launchDebugger(options)).to.eventually.rejectedWith('Please open a workspace'); + + debugService.verifyAll(); + }); + }); + + test('Tries launch.json first', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + const expected = getDefaultDebugConfig(); + expected.name = 'spam'; + 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(); + }); + + test('Full debug config', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + const expected = { + name: 'my tests', + type: PythonDebuggerTypeName, + request: 'launch', + 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: { + PYTHONPATH: 'one/two/three', + SPAM: 'EGGS', + TEST_RUN_PIPE: 'pytestPort', + RUN_TEST_IDS_PIPE: 'runTestIdsPort', + }, + envFile: 'some/dir/.env', + redirectOutput: false, + debugStdLib: true, + // added by LaunchConfigurationResolver: + internalConsoleOptions: 'neverOpen', + subProcess: true, + purpose: [], + }; + setupSuccess(options, 'unittest', expected, [ + { + name: 'my tests', + type: PythonDebuggerTypeName, + request: 'test', + pythonPath: expected.python, + stopOnEntry: expected.stopOnEntry, + showReturnValue: expected.showReturnValue, + console: expected.console, + cwd: expected.cwd, + env: expected.env, + envFile: expected.envFile, + redirectOutput: expected.redirectOutput, + debugStdLib: expected.debugStdLib, + }, + ]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Uses first entry', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + const expected = getDefaultDebugConfig(); + expected.name = 'spam1'; + setupSuccess(options, 'unittest', expected, [ + { name: 'spam1', type: PythonDebuggerTypeName, request: 'test' }, + { name: 'spam2', type: PythonDebuggerTypeName, request: 'test' }, + { name: 'spam3', type: PythonDebuggerTypeName, request: 'test' }, + ]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Handles bad JSON', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + const expected = getDefaultDebugConfig(); + setupSuccess(options, 'unittest', expected, ']'); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + const malformedFiles = [ + '// test 1', + '// test 2 \n\ + { \n\ + "name": "spam", \n\ + "type": "debugpy", \n\ + "request": "test" \n\ + } \n\ + ', + '// test 3 \n\ + [ \n\ + { \n\ + "name": "spam", \n\ + "type": "debugpy", \n\ + "request": "test" \n\ + } \n\ + ] \n\ + ', + '// test 4 \n\ + { \n\ + "configurations": [ \n\ + { \n\ + "name": "spam", \n\ + "type": "debugpy", \n\ + "request": "test" \n\ + } \n\ + ] \n\ + } \n\ + ', + ]; + for (const text of malformedFiles) { + const testID = text.split('\n')[0].substring(3).trim(); + test(`Handles malformed launch.json - ${testID}`, async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + const expected = getDefaultDebugConfig(); + setupSuccess(options, 'unittest', expected, text); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + } + + test('Handles bad debug config items', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + const expected = getDefaultDebugConfig(); + + setupSuccess(options, 'unittest', expected, [ + {} as DebugConfiguration, + { name: 'spam1' } as DebugConfiguration, + { name: 'spam2', type: PythonDebuggerTypeName } as DebugConfiguration, + { name: 'spam3', request: 'test' } as DebugConfiguration, + { type: PythonDebuggerTypeName } as DebugConfiguration, + { type: PythonDebuggerTypeName, request: 'test' } as DebugConfiguration, + { request: 'test' } as DebugConfiguration, + ]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Handles non-python debug configs', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + const expected = getDefaultDebugConfig(); + setupSuccess(options, 'unittest', expected, [{ name: 'foo', type: 'other', request: 'bar' }]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Handles bogus python debug configs', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + const expected = getDefaultDebugConfig(); + setupSuccess(options, 'unittest', expected, [{ name: 'spam', type: PythonDebuggerTypeName, request: 'bogus' }]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Handles non-test debug config', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + const expected = getDefaultDebugConfig(); + setupSuccess(options, 'unittest', expected, [ + { name: 'spam', type: PythonDebuggerTypeName, request: 'launch' }, + { name: 'spam', type: PythonDebuggerTypeName, request: 'attach' }, + ]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + + test('Handles mixed debug config', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + 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: 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); + + debugService.verifyAll(); + }); + + test('Handles comments', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + const expected = getDefaultDebugConfig(); + expected.name = 'spam'; + expected.stopOnEntry = true; + setupSuccess( + options, + 'unittest', + expected, + ' \n\ + { \n\ + "version": "0.1.0", \n\ + "configurations": [ \n\ + // my thing \n\ + { \n\ + // "test" debug config \n\ + "name": "spam", /* non-empty */ \n\ + "type": "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); + + debugService.verifyAll(); + }); + test('Ensure trailing commands in JSON are handled', async () => { + const workspaceFolder = { name: 'abc', index: 0, uri: Uri.file(__filename) }; + const filename = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); + const jsonc = '{"version":"1234", "configurations":[1,2,],}'; + pathExistsStub.resolves(true); + readFileStub.withArgs(filename).resolves(jsonc); + + const configs = await debugLauncher.readAllDebugConfigs(workspaceFolder); + + expect(configs).to.be.deep.equal([1, 2]); + }); + test('Ensure empty configuration is returned when launch.json cannot be parsed', async () => { + const workspaceFolder = { name: 'abc', index: 0, uri: Uri.file(__filename) }; + const filename = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); + const jsonc = '{"version":"1234"'; + + 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/testConfigurationManager.unit.test.ts b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts new file mode 100644 index 000000000000..1b049d4f3fbe --- /dev/null +++ b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as TypeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; +import { IInstaller, ILogOutputChannel, 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 { TestConfigurationManager } from '../../../../client/testing/common/testConfigurationManager'; +import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/testing/common/types'; + +class MockTestConfigurationManager extends TestConfigurationManager { + // The workspace arg is ignored. + // eslint-disable-next-line class-methods-use-this + public requiresUserToConfigure(): Promise { + throw new Error('Method not implemented.'); + } + + // The workspace arg is ignored. + // eslint-disable-next-line class-methods-use-this + public configure(): Promise { + throw new Error('Method not implemented.'); + } +} + +suite('Unit Test Configuration Manager (unit)', () => { + UNIT_TEST_PRODUCTS.forEach((product) => { + const prods = getNamesAndValues(Product); + const productName = prods.filter((item) => item.value === product)[0]; + suite(productName.name, () => { + const workspaceUri = Uri.file(__dirname); + let manager: TestConfigurationManager; + let configService: TypeMoq.IMock; + + setup(() => { + configService = TypeMoq.Mock.ofType(); + const outputChannel = TypeMoq.Mock.ofType().object; + const installer = TypeMoq.Mock.ofType().object; + const serviceContainer = TypeMoq.Mock.ofType(); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ILogOutputChannel))) + .returns(() => outputChannel); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ITestConfigSettingsService))) + .returns(() => configService.object); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(IInstaller))).returns(() => installer); + manager = new MockTestConfigurationManager( + workspaceUri, + product as UnitTestProduct, + serviceContainer.object, + ); + }); + + test('Enabling a test product shoud disable other products', async () => { + UNIT_TEST_PRODUCTS.filter((item) => item !== product).forEach((productToDisable) => { + configService + .setup((c) => c.disable(TypeMoq.It.isValue(workspaceUri), TypeMoq.It.isValue(productToDisable))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + }); + configService + .setup((c) => c.enable(TypeMoq.It.isValue(workspaceUri), TypeMoq.It.isValue(product))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + await manager.enable(); + configService.verifyAll(); + }); + }); + }); +}); diff --git a/src/test/testing/common/services/configSettingService.unit.test.ts b/src/test/testing/common/services/configSettingService.unit.test.ts new file mode 100644 index 000000000000..d369d7ead825 --- /dev/null +++ b/src/test/testing/common/services/configSettingService.unit.test.ts @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaiPromise from 'chai-as-promised'; +import * as typeMoq from 'typemoq'; +import { Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { Product } from '../../../../client/common/types'; +import { getNamesAndValues } from '../../../../client/common/utils/enum'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { UNIT_TEST_PRODUCTS } from '../../../../client/testing/common/constants'; +import { TestConfigSettingsService } from '../../../../client/testing/common/configSettingService'; +import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/testing/common/types'; +import { BufferedTestConfigSettingsService } from '../../../../client/testing/common/bufferedTestConfigSettingService'; + +use(chaiPromise.default); + +const updateMethods: (keyof Omit)[] = [ + 'updateTestArgs', + 'disable', + 'enable', +]; + +suite('Unit Tests - ConfigSettingsService', () => { + UNIT_TEST_PRODUCTS.forEach((product) => { + const prods = getNamesAndValues(Product); + const productName = prods.filter((item) => item.value === product)[0]; + const workspaceUri = Uri.file(__filename); + updateMethods.forEach((updateMethod) => { + suite(`Test '${updateMethod}' method with ${productName.name}`, () => { + let testConfigSettingsService: ITestConfigSettingsService; + let workspaceService: typeMoq.IMock; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType(); + workspaceService = typeMoq.Mock.ofType(); + + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + testConfigSettingsService = new TestConfigSettingsService(serviceContainer.object); + }); + function getTestArgSetting(prod: UnitTestProduct) { + switch (prod) { + case Product.unittest: + return 'testing.unittestArgs'; + case Product.pytest: + return 'testing.pytestArgs'; + default: + throw new Error('Invalid Test Product'); + } + } + function getTestEnablingSetting(prod: UnitTestProduct) { + switch (prod) { + case Product.unittest: + return 'testing.unittestEnabled'; + case Product.pytest: + return 'testing.pytestEnabled'; + default: + throw new Error('Invalid Test Product'); + } + } + function getExpectedValueAndSettings(): { configValue: any; configName: string } { + switch (updateMethod) { + case 'disable': { + return { configValue: false, configName: getTestEnablingSetting(product) }; + } + case 'enable': { + return { configValue: true, configName: getTestEnablingSetting(product) }; + } + case 'updateTestArgs': { + return { configValue: ['one', 'two', 'three'], configName: getTestArgSetting(product) }; + } + default: { + throw new Error('Invalid Method'); + } + } + } + test('Update Test Arguments with workspace Uri without workspaces', async () => { + const pythonConfig = typeMoq.Mock.ofType(); + workspaceService + .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'))) + .returns(() => pythonConfig.object) + .verifiable(typeMoq.Times.once()); + + const { configValue, configName } = getExpectedValueAndSettings(); + + pythonConfig + .setup((p) => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + if (updateMethod === 'updateTestArgs') { + await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + } else { + await testConfigSettingsService[updateMethod](workspaceUri, product); + } + workspaceService.verifyAll(); + pythonConfig.verifyAll(); + }); + test('Update Test Arguments with workspace Uri with one workspace', async () => { + const workspaceFolder = typeMoq.Mock.ofType(); + workspaceFolder + .setup((w) => w.uri) + .returns(() => workspaceUri) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder.object]) + .verifiable(typeMoq.Times.atLeastOnce()); + + const pythonConfig = typeMoq.Mock.ofType(); + workspaceService + .setup((w) => + w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri)), + ) + .returns(() => pythonConfig.object) + .verifiable(typeMoq.Times.once()); + + const { configValue, configName } = getExpectedValueAndSettings(); + pythonConfig + .setup((p) => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + if (updateMethod === 'updateTestArgs') { + await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + } else { + await testConfigSettingsService[updateMethod](workspaceUri, product); + } + + workspaceService.verifyAll(); + pythonConfig.verifyAll(); + }); + test('Update Test Arguments with workspace Uri with more than one workspace and uri belongs to a workspace', async () => { + const workspaceFolder = typeMoq.Mock.ofType(); + workspaceFolder + .setup((w) => w.uri) + .returns(() => workspaceUri) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder.object, workspaceFolder.object]) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.getWorkspaceFolder(typeMoq.It.isValue(workspaceUri))) + .returns(() => workspaceFolder.object) + .verifiable(typeMoq.Times.once()); + + const pythonConfig = typeMoq.Mock.ofType(); + workspaceService + .setup((w) => + w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri)), + ) + .returns(() => pythonConfig.object) + .verifiable(typeMoq.Times.once()); + + const { configValue, configName } = getExpectedValueAndSettings(); + pythonConfig + .setup((p) => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + if (updateMethod === 'updateTestArgs') { + await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + } else { + await testConfigSettingsService[updateMethod](workspaceUri, product); + } + + workspaceService.verifyAll(); + pythonConfig.verifyAll(); + }); + test('Expect an exception when updating Test Arguments with workspace Uri with more than one workspace and uri does not belong to a workspace', async () => { + const workspaceFolder = typeMoq.Mock.ofType(); + workspaceFolder + .setup((w) => w.uri) + .returns(() => workspaceUri) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.workspaceFolders) + .returns(() => [workspaceFolder.object, workspaceFolder.object]) + .verifiable(typeMoq.Times.atLeastOnce()); + workspaceService + .setup((w) => w.getWorkspaceFolder(typeMoq.It.isValue(workspaceUri))) + .returns(() => undefined) + .verifiable(typeMoq.Times.once()); + + const { configValue } = getExpectedValueAndSettings(); + + const promise = testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); + expect(promise).to.eventually.rejectedWith(); + workspaceService.verifyAll(); + }); + }); + }); + }); +}); + +suite('Unit Tests - BufferedTestConfigSettingsService', () => { + test('config changes are pushed when apply() is called', async () => { + const testDir = '/my/project'; + const newArgs: string[] = ['-x', '--spam=42']; + const cfg = typeMoq.Mock.ofType(undefined, typeMoq.MockBehavior.Strict); + cfg.setup((c) => + c.updateTestArgs( + typeMoq.It.isValue(testDir), + typeMoq.It.isValue(Product.pytest), + typeMoq.It.isValue(newArgs), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + cfg.setup((c) => c.disable(typeMoq.It.isValue(testDir), typeMoq.It.isValue(Product.unittest))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + cfg.setup((c) => c.enable(typeMoq.It.isValue(testDir), typeMoq.It.isValue(Product.pytest))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + const delayed = new BufferedTestConfigSettingsService(); + await delayed.updateTestArgs(testDir, Product.pytest, newArgs); + await delayed.disable(testDir, Product.unittest); + await delayed.enable(testDir, Product.pytest); + await delayed.apply(cfg.object); + + // Ideally we would verify that the ops were applied in their + // original order. Unfortunately, the version of TypeMoq we're + // using does not give us that option. + cfg.verifyAll(); + }); + + test('applied changes are cleared', async () => { + const cfg = typeMoq.Mock.ofType(undefined, typeMoq.MockBehavior.Strict); + cfg.setup((c) => c.enable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + const delayed = new BufferedTestConfigSettingsService(); + await delayed.enable('/my/project', Product.pytest); + await delayed.apply(cfg.object); + await delayed.apply(cfg.object); + + cfg.verifyAll(); + }); +}); diff --git a/src/test/testing/common/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); + pythonExecFactory = serviceContainer.get(IPythonExecutionFactory); + testController = serviceContainer.get(ITestController); + envVarsService = serviceContainer.get(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 + .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 + .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 + .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 + .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 + .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 + .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 + .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 + .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 = {}; + 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/configuration.unit.test.ts b/src/test/testing/configuration.unit.test.ts new file mode 100644 index 000000000000..e259587ecccd --- /dev/null +++ b/src/test/testing/configuration.unit.test.ts @@ -0,0 +1,323 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as typeMoq from 'typemoq'; +import { OutputChannel, Uri, WorkspaceConfiguration } from 'vscode'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; +import { + IConfigurationService, + IInstaller, + ILogOutputChannel, + IPythonSettings, + 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 { TestsHelper } from '../../client/testing/common/testUtils'; +import { + ITestConfigSettingsService, + ITestConfigurationManager, + 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) => { + const prods = getNamesAndValues(Product); + const productName = prods.filter((item) => item.value === product)[0]; + const workspaceUri = Uri.file(__filename); + suite(productName.name, () => { + let testConfigService: typeMoq.IMock; + let workspaceService: typeMoq.IMock; + let factory: typeMoq.IMock; + let testSettingsService: typeMoq.IMock; + let appShell: typeMoq.IMock; + let unitTestSettings: typeMoq.IMock; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType(undefined, typeMoq.MockBehavior.Strict); + const configurationService = typeMoq.Mock.ofType( + undefined, + typeMoq.MockBehavior.Strict, + ); + appShell = typeMoq.Mock.ofType(undefined, typeMoq.MockBehavior.Strict); + const outputChannel = typeMoq.Mock.ofType(undefined, typeMoq.MockBehavior.Strict); + const installer = typeMoq.Mock.ofType(undefined, typeMoq.MockBehavior.Strict); + workspaceService = typeMoq.Mock.ofType(undefined, typeMoq.MockBehavior.Strict); + factory = typeMoq.Mock.ofType(undefined, typeMoq.MockBehavior.Strict); + testSettingsService = typeMoq.Mock.ofType( + undefined, + typeMoq.MockBehavior.Strict, + ); + unitTestSettings = typeMoq.Mock.ofType(); + const pythonSettings = typeMoq.Mock.ofType(undefined, typeMoq.MockBehavior.Strict); + + pythonSettings.setup((p) => p.testing).returns(() => unitTestSettings.object); + configurationService.setup((c) => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); + + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(ILogOutputChannel))) + .returns(() => outputChannel.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(IConfigurationService))) + .returns(() => configurationService.object); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(IApplicationShell))) + .returns(() => appShell.object); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(ITestConfigurationManagerFactory))) + .returns(() => factory.object); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(ITestConfigSettingsService))) + .returns(() => testSettingsService.object); + const commands = typeMoq.Mock.ofType(undefined, typeMoq.MockBehavior.Strict); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(ICommandManager))) + .returns(() => commands.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(ITestsHelper))).returns(() => new TestsHelper()); + testConfigService = typeMoq.Mock.ofType( + UnitTestConfigurationService, + typeMoq.MockBehavior.Loose, + true, + serviceContainer.object, + ); + }); + test('Enable Test when setting testing.promptToConfigure is enabled', async () => { + const configMgr = typeMoq.Mock.ofType( + undefined, + typeMoq.MockBehavior.Strict, + ); + configMgr + .setup((c) => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + factory + .setup((f) => f.create(workspaceUri, product)) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + const workspaceConfig = typeMoq.Mock.ofType( + undefined, + typeMoq.MockBehavior.Strict, + ); + workspaceService + .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + workspaceConfig + .setup((w) => w.get(typeMoq.It.isValue('testing.promptToConfigure'))) + .returns(() => true) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.enableTest(workspaceUri, product); + + configMgr.verifyAll(); + factory.verifyAll(); + workspaceService.verifyAll(); + workspaceConfig.verifyAll(); + }); + test('Enable Test when setting testing.promptToConfigure is disabled', async () => { + const configMgr = typeMoq.Mock.ofType( + undefined, + typeMoq.MockBehavior.Strict, + ); + configMgr + .setup((c) => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + factory + .setup((f) => f.create(workspaceUri, product)) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + const workspaceConfig = typeMoq.Mock.ofType( + undefined, + typeMoq.MockBehavior.Strict, + ); + workspaceService + .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + workspaceConfig + .setup((w) => w.get(typeMoq.It.isValue('testing.promptToConfigure'))) + .returns(() => false) + .verifiable(typeMoq.Times.once()); + + workspaceConfig + .setup((w) => + w.update(typeMoq.It.isValue('testing.promptToConfigure'), typeMoq.It.isValue(undefined)), + ) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.enableTest(workspaceUri, product); + + configMgr.verifyAll(); + factory.verifyAll(); + workspaceService.verifyAll(); + workspaceConfig.verifyAll(); + }); + test('Enable Test when setting testing.promptToConfigure is disabled and fail to update the settings', async () => { + const configMgr = typeMoq.Mock.ofType( + undefined, + typeMoq.MockBehavior.Strict, + ); + configMgr + .setup((c) => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + + factory + .setup((f) => f.create(workspaceUri, product)) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + const workspaceConfig = typeMoq.Mock.ofType( + undefined, + typeMoq.MockBehavior.Strict, + ); + workspaceService + .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + workspaceConfig + .setup((w) => w.get(typeMoq.It.isValue('testing.promptToConfigure'))) + .returns(() => false) + .verifiable(typeMoq.Times.once()); + + const errorMessage = 'Update Failed'; + const updateFailError = new Error(errorMessage); + workspaceConfig + .setup((w) => + w.update(typeMoq.It.isValue('testing.promptToConfigure'), typeMoq.It.isValue(undefined)), + ) + .returns(() => Promise.reject(updateFailError)) + .verifiable(typeMoq.Times.once()); + + const promise = testConfigService.target.enableTest(workspaceUri, product); + + await expect(promise).to.eventually.be.rejectedWith(errorMessage); + configMgr.verifyAll(); + factory.verifyAll(); + workspaceService.verifyAll(); + workspaceConfig.verifyAll(); + }); + test('Select Test runner displays 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(2)) + .verifiable(typeMoq.Times.once()); + + await testConfigService.target.selectTestRunner(placeHolder); + appShell.verifyAll(); + }); + test('Ensure selected item is returned', async () => { + const placeHolder = 'Some message'; + const indexes = [Product.unittest, Product.pytest]; + appShell + .setup((s) => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) + .callback((items) => expect(items).be.lengthOf(2)) + .returns((items) => items[indexes.indexOf(product)]) + .verifiable(typeMoq.Times.once()); + + const selectedItem = await testConfigService.target.selectTestRunner(placeHolder); + expect(selectedItem).to.be.equal(product); + appShell.verifyAll(); + }); + test('Ensure undefined is returned when nothing is selected', async () => { + const placeHolder = 'Some message'; + appShell + .setup((s) => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) + .returns(() => Promise.resolve(undefined)) + .verifiable(typeMoq.Times.once()); + + const selectedItem = await testConfigService.target.selectTestRunner(placeHolder); + expect(selectedItem).to.be.equal(undefined, 'invalid value'); + appShell.verifyAll(); + }); + test('Correctly returns hasConfiguredTests', () => { + let enabled = false; + unitTestSettings.setup((u) => u.unittestEnabled).returns(() => false); + unitTestSettings.setup((u) => u.pytestEnabled).returns(() => enabled); + + 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); + + const workspaceConfig = typeMoq.Mock.ofType( + undefined, + typeMoq.MockBehavior.Strict, + ); + workspaceConfig + .setup((w) => w.get(typeMoq.It.isAny())) + .returns(() => true) + .verifiable(typeMoq.Times.once()); + workspaceService + .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + .returns(() => workspaceConfig.object) + .verifiable(typeMoq.Times.once()); + + appShell + .setup((s) => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) + .verifiable(typeMoq.Times.never()); + + let selectTestRunnerInvoked = false; + testConfigService.callBase = false; + testConfigService + .setup((t) => t.selectTestRunner(typeMoq.It.isAny())) + .returns(() => { + selectTestRunnerInvoked = true; + return Promise.resolve(product); + }); + + const configMgr = typeMoq.Mock.ofType(); + factory + .setup((f) => + f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product), typeMoq.It.isAny()), + ) + .returns(() => configMgr.object) + .verifiable(typeMoq.Times.once()); + + configMgr + .setup((c) => c.configure(typeMoq.It.isValue(workspaceUri))) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + configMgr + .setup((c) => c.enable()) + .returns(() => Promise.resolve()) + .verifiable(typeMoq.Times.once()); + const configManagersToVerify: typeof configMgr[] = [configMgr]; + + await testConfigService.target.promptToEnableAndConfigureTestFramework(workspaceUri); + + expect(selectTestRunnerInvoked).to.be.equal(true, 'Select Test Runner not invoked'); + appShell.verifyAll(); + factory.verifyAll(); + for (const item of configManagersToVerify) { + item.verifyAll(); + } + workspaceConfig.verifyAll(); + }); + }); + }); +}); diff --git a/src/test/testing/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; + 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(); + 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 new file mode 100644 index 000000000000..493dfcc00b95 --- /dev/null +++ b/src/test/testing/configurationFactory.unit.test.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as typeMoq from 'typemoq'; +import { OutputChannel, Uri } from 'vscode'; +import { IInstaller, ILogOutputChannel, Product } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { ITestConfigSettingsService, ITestConfigurationManagerFactory } from '../../client/testing/common/types'; +import { TestConfigurationManagerFactory } from '../../client/testing/configurationFactory'; +import * as pytest from '../../client/testing/configuration/pytest/testConfigurationManager'; +import * as unittest from '../../client/testing/configuration/unittest/testConfigurationManager'; + +use(chaiAsPromised.default); + +suite('Unit Tests - ConfigurationManagerFactory', () => { + let factory: ITestConfigurationManagerFactory; + setup(() => { + const serviceContainer = typeMoq.Mock.ofType(); + const outputChannel = typeMoq.Mock.ofType(); + const installer = typeMoq.Mock.ofType(); + const testConfigService = typeMoq.Mock.ofType(); + + 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))) + .returns(() => testConfigService.object); + factory = new TestConfigurationManagerFactory(serviceContainer.object); + }); + test('Create Unit Test Configuration', async () => { + const configMgr = factory.create(Uri.file(__filename), Product.unittest); + expect(configMgr).to.be.instanceOf(unittest.ConfigurationManager); + }); + test('Create pytest Configuration', async () => { + const configMgr = factory.create(Uri.file(__filename), Product.pytest); + expect(configMgr).to.be.instanceOf(pytest.ConfigurationManager); + }); +}); diff --git a/src/test/testing/serviceRegistry.ts b/src/test/testing/serviceRegistry.ts new file mode 100644 index 000000000000..231716b653ba --- /dev/null +++ b/src/test/testing/serviceRegistry.ts @@ -0,0 +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 { IInterpreterHelper } from '../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../client/interpreter/helpers'; +import { TestsHelper } from '../../client/testing/common/testUtils'; +import { ITestsHelper } from '../../client/testing/common/types'; +import { getPythonSemVer } from '../common'; +import { IocContainer } from '../serviceRegistry'; + +export class UnitTestIocContainer extends IocContainer { + public async getPythonMajorVersion(resource: Uri): Promise { + const procServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + const procService = await procServiceFactory.create(resource); + const pythonVersion = await getPythonSemVer(procService); + if (pythonVersion) { + return pythonVersion.major; + } + return -1; // log warning already issued by underlying functions... + } + + public registerTestsHelper(): void { + this.serviceManager.addSingleton(ITestsHelper, TestsHelper); + } + + public registerInterpreterStorageTypes(): void { + this.serviceManager.add(IInterpreterHelper, InterpreterHelper); + } +} 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 "", line 1, in \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 \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; + + setup(() => { + coverageHandler = new TestCoverageHandler(); + runInstanceMock = typemoq.Mock.ofType(); + }); + + 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; + let testItemIndexMock: typemoq.IMock; + let testItemCollectionMock: typemoq.IMock; + let workspaceUri: Uri; + let testProvider: TestProvider; + let cancelationToken: CancellationToken; + + setup(() => { + discoveryHandler = new TestDiscoveryHandler(); + testControllerMock = typemoq.Mock.ofType(); + testItemIndexMock = typemoq.Mock.ofType(); + testItemCollectionMock = typemoq.Mock.ofType(); + + // 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(); + 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(); + 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(); + 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(); + 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(); + 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; + let testItemIndexMock: typemoq.IMock; + let runInstanceMock: typemoq.IMock; + let mockTestItem: TestItem; + let mockParentItem: TestItem; + + setup(() => { + executionHandler = new TestExecutionHandler(); + testControllerMock = typemoq.Mock.ofType(); + testItemIndexMock = typemoq.Mock.ofType(); + runInstanceMock = typemoq.Mock.ofType(); + + 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(); + 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; + let mockTestItem1: TestItem; + let mockTestItem2: TestItem; + let mockParentItem: TestItem; + + setup(() => { + testItemIndex = new TestItemIndex(); + testControllerMock = typemoq.Mock.ofType(); + + // 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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 { + 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": "(, AssertionError('1 != 0'), )", "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(); + let adapter: PytestTestDiscoveryAdapter; + let execService: typeMoq.IMock; + let deferred: Deferred; + let expectedPath: string; + let uri: Uri; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let expectedExtraVariables: Record; + let mockProc: MockChildProcess; + let deferred2: Deferred; + 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(); + 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(); + 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>(() => { + /* 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(); + 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((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(); + 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((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(); + 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((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(); + 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((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(); + 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(); + 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>(), + dispose: () => { + /* no-body */ + }, + }; + }); + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + 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(); + let adapter: PytestTestExecutionAdapter; + let execService: typeMoq.IMock; + let deferred: Deferred; + let deferred4: Deferred; + let debugLauncher: typeMoq.IMock; + (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>(() => { + /* no op */ + }); + deferred4 = createDeferred(); + execService = typeMoq.Mock.ofType(); + 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(); + + // added + utilsWriteTestIdsFileStub = sinon.stub(util, 'writeTestIdsFile'); + debugLauncher = typeMoq.Mock.ofType(); + 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(); + 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.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(); + 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.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((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(); + 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.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((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 + .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) => { + 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((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(); + 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.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((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(); + 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.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((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 + .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) => { + // 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(); + 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.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((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.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) => { + 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; + let testControllerMock: typemoq.IMock; + 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(); + 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(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + + // define functions within runInstance + runInstance = typemoq.Mock.ofType(); + 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(); + 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(); + 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 = ['pytest', 'unittest']; + +suite('Execution Flow Run Adapters', () => { + // define suit level variables + let configService: IConfigurationService; + let execFactoryStub = typeMoq.Mock.ofType(); + let execServiceStub: typeMoq.IMock; + // let deferred: Deferred; + let debugLauncher: typeMoq.IMock; + (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(); + 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(); + execFactoryStub = typeMoq.Mock.ofType(); + + // 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(); + 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(); + 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 | 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>>().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(); + 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 | 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>>().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(); + 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(), + runIdToVSid: new Map(), + runIdToTestItem: new Map(), + 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().object, + pythonExecFactory: typemoq.Mock.ofType().object, + debugLauncher: typemoq.Mock.ofType().object, + }; +} + +/** + * Creates a mock TestRun with common setup methods. + * @returns A TypeMoq mock of TestRun + */ +export function createMockTestRun(): typemoq.IMock { + const runMock = typemoq.Mock.ofType(); + 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; + let execFactory = typeMoq.Mock.ofType(); + let deferred: Deferred; + let expectedExtraVariables: Record; + 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>(() => { + /* no op */ + }); + execService = typeMoq.Mock.ofType(); + 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(); + 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>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is((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>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is((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(); + 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(); + 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>(), + dispose: () => { + /* no-body */ + }, + }; + }); + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType(); + 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>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is((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>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is((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(); + let adapter: UnittestTestExecutionAdapter; + let execService: typeMoq.IMock; + let deferred: Deferred; + let deferred4: Deferred; + let debugLauncher: typeMoq.IMock; + (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>(() => { + /* no op */ + }); + deferred4 = createDeferred(); + execService = typeMoq.Mock.ofType(); + 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(); + + // added + utilsWriteTestIdsFileStub = sinon.stub(util, 'writeTestIdsFile'); + debugLauncher = typeMoq.Mock.ofType(); + 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(); + 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.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(); + 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.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((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(); + 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.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((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 + .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) => { + 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((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(); + 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.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((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(); + 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.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((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(); + 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.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((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 + .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) => { + // 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 | undefined; + utilsStartRunResultNamedPipeStub.callsFake((_callback: unknown, deferred: Deferred, _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(); + 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 + .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 }[] = []; + let execFactory: typemoq.IMock; + + // 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 | undefined, + properties: unknown, + ) => { + telemetryEvent.push({ + eventName, + properties: properties as Record, + }); + }; + + 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(); + 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((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; + let testControllerMock: typemoq.IMock; + let telemetryEvent: { eventName: EventName; properties: Record }[] = []; + let resultResolver: ResultResolver.PythonResultResolver; + let execFactory: typemoq.IMock; + + // 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 | undefined, + properties: unknown, + ) => { + telemetryEvent.push({ + eventName, + properties: properties as Record, + }); + }; + + executionTestsStub = sandbox.stub(UnittestTestExecutionAdapter.prototype, 'runTests'); + sendTelemetryStub = sandbox.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + execFactory = typemoq.Mock.ofType(); + runInstance = typemoq.Mock.ofType(); + + 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(); + + 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(); + 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((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(); + 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/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 3805ab911dfd..85308213fd56 100644 --- a/src/test/textUtils.ts +++ b/src/test/textUtils.ts @@ -18,9 +18,10 @@ export function compareFiles(expectedContent: string, actualContent: string) { expect(e, `Difference at line ${i}`).to.be.equal(a); } - expect(actualLines.length, + expect( + 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 a295f2938212..dc4e79cbbff3 100644 --- a/src/test/unittests.ts +++ b/src/test/unittests.ts @@ -1,40 +1,70 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -// tslint:disable:no-any no-require-imports no-var-requires - -if ((Reflect as any).metadata === undefined) { - require('reflect-metadata'); -} - -process.env.VSC_PYTHON_CI_TEST = '1'; -process.env.VSC_PYTHON_UNIT_TEST = '1'; - -import { setUpDomEnvironment } 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, filepath) => { - return origRequire.call(context, filepath); - }; - Module.prototype.require = function (filepath) { - if (filepath.endsWith('.css') || filepath.endsWith('.svg')) { - return ''; - } - // tslint:disable-next-line:no-invalid-this - return _require(this, filepath); - }; -})(); - -// nteract/transforms-full expects to run in the browser so we have to fake -// parts of the browser here. -setUpDomEnvironment(); -initialize(); +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +'use strict'; + +// Not sure why but on windows, if you execute a process from the System32 directory, it will just crash Node. +// Not throw an exception, just make node exit. +// However if a system32 process is run first, everything works. +import * as child_process from 'child_process'; +import * as os from 'os'; +if (os.platform() === 'win32') { + const proc = child_process.spawn('C:\\Windows\\System32\\Reg.exe', ['/?']); + proc.on('error', () => { + console.error('error during reg.exe'); + }); +} + +if ((Reflect as any).metadata === undefined) { + require('reflect-metadata'); +} + +process.env.VSC_PYTHON_CI_TEST = '1'; +process.env.VSC_PYTHON_UNIT_TEST = '1'; +process.env.NODE_ENV = 'production'; // Make sure react is using production bits or we can run out of memory. + +import { initialize } from './vscode-mock'; + +// Custom module loader so we skip .css files that break non webpack wrapped compiles + +const Module = require('module'); + +// Required for DS functional tests. + +(function () { + const origRequire = Module.prototype.require; + const _require = (context: any, filepath: any) => { + return origRequire.call(context, filepath); + }; + Module.prototype.require = function (filepath: string) { + if (filepath.endsWith('.css') || filepath.endsWith('.svg')) { + return ''; + } + if (filepath.startsWith('expose-loader?')) { + // Pull out the thing to expose + const queryEnd = filepath.indexOf('!'); + if (queryEnd >= 0) { + const query = filepath.substring('expose-loader?'.length, queryEnd); + + (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. + + const result = _require(this, filepath); + + // However it doesn't look in the 'window' object later. we have to move it to + // the globals when in node.js + if ((window as any).Slick) { + (global as any).Slick = (window as any).Slick; + } + + return result; + } + + return _require(this, filepath); + }; +})(); + +initialize(); diff --git a/src/test/unittests/argsService.test.ts b/src/test/unittests/argsService.test.ts deleted file mode 100644 index 3be5899ce634..000000000000 --- a/src/test/unittests/argsService.test.ts +++ /dev/null @@ -1,177 +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 { ILogger, Product } from '../../client/common/types'; -import { getNamesAndValues } from '../../client/common/utils/enum'; -import { IServiceContainer } from '../../client/ioc/types'; -import { ArgumentsHelper } from '../../client/unittests/common/argumentsHelper'; -import { ArgumentsService as NoseTestArgumentsService } from '../../client/unittests/nosetest/services/argsService'; -import { ArgumentsService as PyTestArgumentsService } from '../../client/unittests/pytest/services/argsService'; -import { IArgumentsHelper, IArgumentsService } from '../../client/unittests/types'; -import { ArgumentsService as UnitTestArgumentsService } from '../../client/unittests/unittest/services/argsService'; -import { PYTHON_PATH } from '../common'; - -suite('ArgsService: Common', () => { - [Product.unittest, Product.nosetest, Product.pytest] - .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(5000); - const serviceContainer = typeMoq.Mock.ofType(); - const logger = typeMoq.Mock.ofType(); - - serviceContainer - .setup(s => s.get(typeMoq.It.isValue(ILogger), typeMoq.It.isAny())) - .returns(() => logger.object); - - const argsHelper = new ArgumentsHelper(serviceContainer.object); - - 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 begining 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); -} - -function getMatches(pattern, str) { - 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((items, item) => items.indexOf(item) === -1 ? items.concat([item]) : items, []); -} diff --git a/src/test/unittests/banners/languageServerSurvey.unit.test.ts b/src/test/unittests/banners/languageServerSurvey.unit.test.ts deleted file mode 100644 index c77fe4682b7e..000000000000 --- a/src/test/unittests/banners/languageServerSurvey.unit.test.ts +++ /dev/null @@ -1,181 +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 { SemVer } from 'semver'; -import * as typemoq from 'typemoq'; -import { FolderVersionPair, ILanguageServerFolderService } from '../../../client/activation/types'; -import { IApplicationShell } from '../../../client/common/application/types'; -import { IBrowserService, IConfigurationService, IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; -import { LanguageServerSurveyBanner, LSSurveyStateKeys } from '../../../client/languageServices/languageServerSurveyBanner'; - -suite('Language Server Survey Banner', () => { - let config: typemoq.IMock; - let appShell: typemoq.IMock; - let browser: typemoq.IMock; - let lsService: typemoq.IMock; - - const message = 'Can you please take 2 minutes to tell us how the Experimental Debugger is working for you?'; - const yes = 'Yes, take survey now'; - const no = 'No, thanks'; - - setup(() => { - config = typemoq.Mock.ofType(); - appShell = typemoq.Mock.ofType(); - browser = typemoq.Mock.ofType(); - lsService = typemoq.Mock.ofType(); - }); - test('Is debugger enabled upon creation?', () => { - const enabledValue: boolean = true; - const attemptCounter: number = 0; - const completionsCount: number = 0; - const testBanner: LanguageServerSurveyBanner = preparePopup(attemptCounter, completionsCount, enabledValue, 0, 100, appShell.object, browser.object, lsService.object); - expect(testBanner.enabled).to.be.equal(true, 'Sampling 100/100 should always enable the banner.'); - }); - test('Do not show 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 = true; - const attemptCounter: number = 0; - const completionsCount: number = 0; - const testBanner: LanguageServerSurveyBanner = preparePopup(attemptCounter, completionsCount, enabledValue, 0, 0, appShell.object, browser.object, lsService.object); - testBanner.showBanner().ignoreErrors(); - }); - test('shouldShowBanner must return false when Banner is implicitly disabled by sampling', () => { - const enabledValue: boolean = true; - const attemptCounter: number = 0; - const completionsCount: number = 0; - const testBanner: LanguageServerSurveyBanner = preparePopup(attemptCounter, completionsCount, enabledValue, 0, 0, appShell.object, browser.object, lsService.object); - expect(testBanner.enabled).to.be.equal(false, 'We implicitly disabled the banner, it should never show.'); - }); - - const languageServerVersions: string[] = [ - '1.2.3', - '1.2.3-alpha', - '0.0.1234567890', - '1234567890.0.1', - '1.0.1-alpha+2', - '22.4.999-rc.6' - ]; - languageServerVersions.forEach(async (languageServerVersion: string) => { - test(`Survey URL is as expected for Language Server version '${languageServerVersion}'.`, async () => { - const enabledValue: boolean = true; - const attemptCounter: number = 42; - const completionsCount: number = 0; - - // the expected URI as provided in issue #2630 - // with mocked-up test replacement values - - const expectedUri: string = `https://www.research.net/r/LJZV9BZ?n=${attemptCounter}&v=${encodeURIComponent(languageServerVersion)}`; - - const lsFolder: FolderVersionPair = { - path: '/some/path', - version: new SemVer(languageServerVersion, true) - }; - // language service will get asked for the current Language - // Server directory installed. This in turn will give the tested - // code the version via the .version member of lsFolder. - lsService.setup(f => f.getCurrentLanguageServerDirectory()) - .returns(() => { - return Promise.resolve(lsFolder); - }) - .verifiable(typemoq.Times.once()); - - // The browser service will be asked to launch a URI that is - // built using similar constants to those found in this test - // suite. The exact built URI should be received in a single call - // to launch. - let receivedUri: string = ''; - browser.setup(b => b.launch( - typemoq.It.is((a: string) => { - receivedUri = a; - return a === expectedUri; - })) - ) - .verifiable(typemoq.Times.once()); - - const testBanner: LanguageServerSurveyBanner = preparePopup(attemptCounter, completionsCount, enabledValue, 0, 0, appShell.object, browser.object, lsService.object); - 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. - lsService.verifyAll(); - browser.verifyAll(); - - lsService.reset(); - browser.reset(); - }); - }); -}); - -function preparePopup( - attemptCounter: number, - completionsCount: number, - enabledValue: boolean, - minCompletionCount: number, - maxCompletionCount: number, - appShell: IApplicationShell, - browser: IBrowserService, - lsService: ILanguageServerFolderService -): LanguageServerSurveyBanner { - - const myfactory: typemoq.IMock = typemoq.Mock.ofType(); - const enabledValState: typemoq.IMock> = typemoq.Mock.ofType>(); - const attemptCountState: typemoq.IMock> = typemoq.Mock.ofType>(); - const completionCountState: typemoq.IMock> = typemoq.Mock.ofType>(); - 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(); - }); - - attemptCountState.setup(a => a.updateValue(typemoq.It.isAnyNumber())).returns(() => { - attemptCounter += 1; - return Promise.resolve(); - }); - - completionCountState.setup(a => a.updateValue(typemoq.It.isAnyNumber())).returns(() => { - completionsCount += 1; - return Promise.resolve(); - }); - - enabledValState.setup(a => a.value).returns(() => enabledValue); - attemptCountState.setup(a => a.value).returns(() => attemptCounter); - completionCountState.setup(a => a.value).returns(() => completionsCount); - - myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(LSSurveyStateKeys.ShowBanner), - typemoq.It.isValue(true))).returns(() => { - return enabledValState.object; - }); - myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(LSSurveyStateKeys.ShowBanner), - typemoq.It.isValue(false))).returns(() => { - return enabledValState.object; - }); - myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(LSSurveyStateKeys.ShowAttemptCounter), - typemoq.It.isAnyNumber())).returns(() => { - return attemptCountState.object; - }); - myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(LSSurveyStateKeys.ShowAfterCompletionCount), - typemoq.It.isAnyNumber())).returns(() => { - return completionCountState.object; - }); - return new LanguageServerSurveyBanner( - appShell, - myfactory.object, - browser, - lsService, - minCompletionCount, - maxCompletionCount); -} diff --git a/src/test/unittests/banners/proposeNewLanguageServerBanner.unit.test.ts b/src/test/unittests/banners/proposeNewLanguageServerBanner.unit.test.ts deleted file mode 100644 index e11c7e75d637..000000000000 --- a/src/test/unittests/banners/proposeNewLanguageServerBanner.unit.test.ts +++ /dev/null @@ -1,85 +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 { IApplicationShell } from '../../../client/common/application/types'; -import { IConfigurationService, IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; -import { ProposeLanguageServerBanner, ProposeLSStateKeys } from '../../../client/languageServices/proposeLanguageServerBanner'; - -suite('Propose New Language Server Banner', () => { - let config: typemoq.IMock; - let appShell: typemoq.IMock; - const message = 'Try out Preview of our new Python Language Server to get richer and faster IntelliSense completions, and syntax errors as you type.'; - const yes = 'Try it now'; - const no = 'No thanks'; - const later = 'Remind me Later'; - - setup(() => { - config = typemoq.Mock.ofType(); - appShell = typemoq.Mock.ofType(); - }); - test('Is debugger enabled upon creation?', () => { - const enabledValue: boolean = true; - const testBanner: ProposeLanguageServerBanner = preparePopup(enabledValue, 100, appShell.object, config.object); - expect(testBanner.enabled).to.be.equal(true, 'Sampling 100/100 should always enable the banner.'); - }); - test('Do not show banner when it is disabled', () => { - 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 enabled: boolean = true; - const testBanner: ProposeLanguageServerBanner = preparePopup(enabled, 0, appShell.object, config.object); - testBanner.showBanner().ignoreErrors(); - }); - test('shouldShowBanner must return false when Banner is implicitly disabled by sampling', () => { - const enabled: boolean = true; - const testBanner: ProposeLanguageServerBanner = preparePopup(enabled, 0, appShell.object, config.object); - expect(testBanner.enabled).to.be.equal(false, 'We implicitly disabled the banner, it should never show.'); - }); - test('shouldShowBanner must return false when Banner is explicitly disabled', async () => { - const enabled: boolean = true; - const testBanner: ProposeLanguageServerBanner = preparePopup(enabled, 100, appShell.object, config.object); - - expect(await testBanner.shouldShowBanner()).to.be.equal(true, '100% sample size should always make the banner enabled.'); - await testBanner.disable(); - expect(await testBanner.shouldShowBanner()).to.be.equal(false, 'Explicitly disabled banner shouldShowBanner != false.'); - }); -}); - -function preparePopup(enabledValue: boolean, sampleValue: number, appShell: IApplicationShell, config: IConfigurationService): ProposeLanguageServerBanner { - const myfactory: typemoq.IMock = typemoq.Mock.ofType(); - const val: typemoq.IMock> = typemoq.Mock.ofType>(); - 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; - }); - return new ProposeLanguageServerBanner( - appShell, - myfactory.object, - config, - sampleValue); -} diff --git a/src/test/unittests/common/argsHelper.unit.test.ts b/src/test/unittests/common/argsHelper.unit.test.ts deleted file mode 100644 index 29a9ac3261d1..000000000000 --- a/src/test/unittests/common/argsHelper.unit.test.ts +++ /dev/null @@ -1,111 +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 * as typeMoq from 'typemoq'; -import { ILogger } from '../../../client/common/types'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { ArgumentsHelper } from '../../../client/unittests/common/argumentsHelper'; -import { IArgumentsHelper } from '../../../client/unittests/types'; -const assertArrays = require('chai-arrays'); -use(assertArrays); - -suite('Unit Tests - Arguments Helper', () => { - let argsHelper: IArgumentsHelper; - setup(() => { - const serviceContainer = typeMoq.Mock.ofType(); - const logger = typeMoq.Mock.ofType(); - - serviceContainer - .setup(s => s.get(typeMoq.It.isValue(ILogger), typeMoq.It.isAny())) - .returns(() => logger.object); - - argsHelper = new ArgumentsHelper(serviceContainer.object); - }); - - 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 ineline 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 multiplle 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('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/unittests/common/debugLauncher.test.ts b/src/test/unittests/common/debugLauncher.test.ts deleted file mode 100644 index ff72867a238c..000000000000 --- a/src/test/unittests/common/debugLauncher.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 - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, Uri, WorkspaceFolder } from 'vscode'; -import { IDebugService, IWorkspaceService } from '../../../client/common/application/types'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import '../../../client/common/extensions'; -import { IConfigurationService, IPythonSettings, IUnitTestSettings } from '../../../client/common/types'; -import { DebuggerTypeName } from '../../../client/debugger/constants'; -import { DebugOptions } from '../../../client/debugger/types'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { DebugLauncher } from '../../../client/unittests/common/debugLauncher'; -import { TestProvider } from '../../../client/unittests/common/types'; - -use(chaiAsPromised); - -// tslint:disable-next-line:max-func-body-length -suite('Unit Tests - Debug Launcher', () => { - let unitTestSettings: TypeMoq.IMock; - let debugLauncher: DebugLauncher; - let debugService: TypeMoq.IMock; - let workspaceService: TypeMoq.IMock; - let settings: TypeMoq.IMock; - setup(async () => { - const serviceContainer = TypeMoq.Mock.ofType(); - const configService = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); - - debugService = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDebugService))).returns(() => debugService.object); - - workspaceService = TypeMoq.Mock.ofType(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - - settings = TypeMoq.Mock.ofType(); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - - unitTestSettings = TypeMoq.Mock.ofType(); - settings.setup(p => p.unitTest).returns(() => unitTestSettings.object); - - debugLauncher = new DebugLauncher(serviceContainer.object); - }); - function setupDebugManager(workspaceFolder: WorkspaceFolder, name: string, type: string, - request: string, program: string, cwd: string, - args: string[], console, debugOptions: DebugOptions[], - testProvider: TestProvider) { - - const envFile = __filename; - settings.setup(p => p.envFile).returns(() => envFile); - const debugArgs = testProvider === 'unittest' ? args.filter(item => item !== '--debug') : args; - - debugService.setup(d => d.startDebugging(TypeMoq.It.isValue(workspaceFolder), - TypeMoq.It.isObjectWith({ name, type, request, program, cwd, args: debugArgs, console, envFile, debugOptions }))) - .returns(() => Promise.resolve(undefined as any)) - .verifiable(TypeMoq.Times.once()); - } - function createWorkspaceFolder(folderPath: string): WorkspaceFolder { - return { index: 0, name: path.basename(folderPath), uri: Uri.file(folderPath) }; - } - function getTestLauncherScript(testProvider: TestProvider) { - switch (testProvider) { - case 'unittest': { - return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'visualstudio_py_testlauncher.py'); - } - case 'pytest': - case 'nosetest': { - return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'testlauncher.py'); - } - default: { - throw new Error(`Unknown test provider '${testProvider}'`); - } - } - } - const testProviders: TestProvider[] = ['nosetest', 'pytest', 'unittest']; - testProviders.forEach(testProvider => { - const testTitleSuffix = `(Test Framework '${testProvider}')`; - const testLaunchScript = getTestLauncherScript(testProvider); - const debuggerType = DebuggerTypeName; - - test(`Must launch debugger ${testTitleSuffix}`, async () => { - workspaceService.setup(u => u.hasWorkspaceFolders).returns(() => true); - const workspaceFolders = [createWorkspaceFolder('one/two/three'), createWorkspaceFolder('five/six/seven')]; - workspaceService.setup(u => u.workspaceFolders).returns(() => workspaceFolders); - workspaceService.setup(u => u.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolders[0]); - - const args = ['/one/two/three/testfile.py']; - const cwd = workspaceFolders[0].uri.fsPath; - const program = testLaunchScript; - setupDebugManager(workspaceFolders[0], 'Debug Unit Test', debuggerType, 'launch', program, cwd, args, 'none', [DebugOptions.RedirectOutput], testProvider); - - debugLauncher.launchDebugger({ cwd, args, testProvider }).ignoreErrors(); - debugService.verifyAll(); - }); - test(`Must launch debugger with arguments ${testTitleSuffix}`, async () => { - workspaceService.setup(u => u.hasWorkspaceFolders).returns(() => true); - const workspaceFolders = [createWorkspaceFolder('one/two/three'), createWorkspaceFolder('five/six/seven')]; - workspaceService.setup(u => u.workspaceFolders).returns(() => workspaceFolders); - workspaceService.setup(u => u.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolders[0]); - - const args = ['/one/two/three/testfile.py', '--debug', '1']; - const cwd = workspaceFolders[0].uri.fsPath; - const program = testLaunchScript; - setupDebugManager(workspaceFolders[0], 'Debug Unit Test', debuggerType, 'launch', program, cwd, args, 'none', [DebugOptions.RedirectOutput], testProvider); - - debugLauncher.launchDebugger({ cwd, args, testProvider }).ignoreErrors(); - debugService.verifyAll(); - }); - test(`Must not launch debugger if cancelled ${testTitleSuffix}`, async () => { - workspaceService.setup(u => u.hasWorkspaceFolders).returns(() => true); - - debugService.setup(d => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined as any)) - .verifiable(TypeMoq.Times.never()); - - const cancellationToken = new CancellationTokenSource(); - cancellationToken.cancel(); - const token = cancellationToken.token; - await expect(debugLauncher.launchDebugger({ cwd: '', args: [], token, testProvider })).to.be.eventually.equal(undefined, 'not undefined'); - debugService.verifyAll(); - }); - test(`Must throw an exception if there are no workspaces ${testTitleSuffix}`, async () => { - workspaceService.setup(u => u.hasWorkspaceFolders).returns(() => false); - - debugService.setup(d => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined as any)) - .verifiable(TypeMoq.Times.never()); - - await expect(debugLauncher.launchDebugger({ cwd: '', args: [], testProvider })).to.eventually.rejectedWith('Please open a workspace'); - debugService.verifyAll(); - }); - }); -}); diff --git a/src/test/unittests/common/managers/testConfigurationManager.unit.test.ts b/src/test/unittests/common/managers/testConfigurationManager.unit.test.ts deleted file mode 100644 index b646dc810de6..000000000000 --- a/src/test/unittests/common/managers/testConfigurationManager.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-any - -import * as TypeMoq from 'typemoq'; -import { OutputChannel, Uri } from 'vscode'; -import { IInstaller, IOutputChannel, Product } from '../../../../client/common/types'; -import { getNamesAndValues } from '../../../../client/common/utils/enum'; -import { IServiceContainer } from '../../../../client/ioc/types'; -import { TEST_OUTPUT_CHANNEL } from '../../../../client/unittests/common/constants'; -import { TestConfigurationManager } from '../../../../client/unittests/common/managers/testConfigurationManager'; -import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/unittests/common/types'; - -class MockTestConfigurationManager extends TestConfigurationManager { - public requiresUserToConfigure(wkspace: Uri): Promise { - throw new Error('Method not implemented.'); - } - public configure(wkspace: any): Promise { - throw new Error('Method not implemented.'); - } -} - -suite('Unit Test Configuration Manager (unit)', () => { - [Product.pytest, Product.unittest, Product.nosetest].forEach(product => { - const prods = getNamesAndValues(Product); - const productName = prods.filter(item => item.value === product)[0]; - suite(productName.name, () => { - const workspaceUri = Uri.file(__dirname); - let manager: TestConfigurationManager; - let configService: TypeMoq.IMock; - - setup(() => { - configService = TypeMoq.Mock.ofType(); - const outputChannel = TypeMoq.Mock.ofType().object; - const installer = TypeMoq.Mock.ofType().object; - const serviceContainer = TypeMoq.Mock.ofType(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(TEST_OUTPUT_CHANNEL))).returns(() => outputChannel); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ITestConfigSettingsService))).returns(() => configService.object); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IInstaller))).returns(() => installer); - manager = new MockTestConfigurationManager(workspaceUri, product as UnitTestProduct, serviceContainer.object); - }); - - test('Enabling a test product shoud disable other products', async () => { - const testProducsToDisable = [Product.pytest, Product.unittest, Product.nosetest] - .filter(item => item !== product) as UnitTestProduct[]; - testProducsToDisable.forEach(productToDisable => { - configService.setup(c => c.disable(TypeMoq.It.isValue(workspaceUri), - TypeMoq.It.isValue(productToDisable))) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - }); - configService.setup(c => c.enable(TypeMoq.It.isValue(workspaceUri), - TypeMoq.It.isValue(product as UnitTestProduct))) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - - await manager.enable(); - configService.verifyAll(); - }); - }); - }); -}); diff --git a/src/test/unittests/common/services/configSettingService.unit.test.ts b/src/test/unittests/common/services/configSettingService.unit.test.ts deleted file mode 100644 index f469bc6ce66b..000000000000 --- a/src/test/unittests/common/services/configSettingService.unit.test.ts +++ /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 no-any - -import { expect, use } from 'chai'; -import * as chaiPromise from 'chai-as-promised'; -import * as typeMoq from 'typemoq'; -import { Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; -import { IWorkspaceService } from '../../../../client/common/application/types'; -import { Product } from '../../../../client/common/types'; -import { getNamesAndValues } from '../../../../client/common/utils/enum'; -import { IServiceContainer } from '../../../../client/ioc/types'; -import { TestConfigSettingsService } from '../../../../client/unittests/common/services/configSettingService'; -import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/unittests/common/types'; - -use(chaiPromise); - -const updateMethods: (keyof ITestConfigSettingsService)[] = ['updateTestArgs', 'disable', 'enable']; - -suite('Unit Tests - ConfigSettingsService', () => { - [Product.pytest, Product.unittest, Product.nosetest].forEach(prodItem => { - const product = prodItem as any as UnitTestProduct; - const prods = getNamesAndValues(Product); - const productName = prods.filter(item => item.value === product)[0]; - const workspaceUri = Uri.file(__filename); - updateMethods.forEach(updateMethod => { - suite(`Test '${updateMethod}' method with ${productName.name}`, () => { - let testConfigSettingsService: ITestConfigSettingsService; - let workspaceService: typeMoq.IMock; - setup(() => { - const serviceContainer = typeMoq.Mock.ofType(); - workspaceService = typeMoq.Mock.ofType(); - - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - testConfigSettingsService = new TestConfigSettingsService(serviceContainer.object); - }); - function getTestArgSetting(prod: UnitTestProduct) { - switch (prod) { - case Product.unittest: - return 'unitTest.unittestArgs'; - case Product.pytest: - return 'unitTest.pyTestArgs'; - case Product.nosetest: - return 'unitTest.nosetestArgs'; - default: - throw new Error('Invalid Test Product'); - } - } - function getTestEnablingSetting(prod: UnitTestProduct) { - switch (prod) { - case Product.unittest: - return 'unitTest.unittestEnabled'; - case Product.pytest: - return 'unitTest.pyTestEnabled'; - case Product.nosetest: - return 'unitTest.nosetestsEnabled'; - default: - throw new Error('Invalid Test Product'); - } - } - function getExpectedValueAndSettings(): { configValue: any; configName: string } { - switch (updateMethod) { - case 'disable': { - return { configValue: false, configName: getTestEnablingSetting(product) }; - } - case 'enable': { - return { configValue: true, configName: getTestEnablingSetting(product) }; - } - case 'updateTestArgs': { - return { configValue: ['one', 'two', 'three'], configName: getTestArgSetting(product) }; - } - default: { - throw new Error('Invalid Method'); - } - } - } - test('Update Test Arguments with workspace Uri without workspaces', async () => { - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => false) - .verifiable(typeMoq.Times.atLeastOnce()); - - const pythonConfig = typeMoq.Mock.ofType(); - workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'))) - .returns(() => pythonConfig.object) - .verifiable(typeMoq.Times.once()); - - const { configValue, configName } = getExpectedValueAndSettings(); - - pythonConfig.setup(p => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - - if (updateMethod === 'updateTestArgs') { - await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); - } else { - await testConfigSettingsService[updateMethod](workspaceUri, product); - } - workspaceService.verifyAll(); - pythonConfig.verifyAll(); - }); - test('Update Test Arguments with workspace Uri with one workspace', async () => { - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(typeMoq.Times.atLeastOnce()); - - const workspaceFolder = typeMoq.Mock.ofType(); - workspaceFolder.setup(w => w.uri) - .returns(() => workspaceUri) - .verifiable(typeMoq.Times.atLeastOnce()); - workspaceService.setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder.object]) - .verifiable(typeMoq.Times.atLeastOnce()); - - const pythonConfig = typeMoq.Mock.ofType(); - workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri))) - .returns(() => pythonConfig.object) - .verifiable(typeMoq.Times.once()); - - const { configValue, configName } = getExpectedValueAndSettings(); - pythonConfig.setup(p => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - - if (updateMethod === 'updateTestArgs') { - await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); - } else { - await testConfigSettingsService[updateMethod](workspaceUri, product); - } - - workspaceService.verifyAll(); - pythonConfig.verifyAll(); - }); - test('Update Test Arguments with workspace Uri with more than one workspace and uri belongs to a workspace', async () => { - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(typeMoq.Times.atLeastOnce()); - - const workspaceFolder = typeMoq.Mock.ofType(); - workspaceFolder.setup(w => w.uri) - .returns(() => workspaceUri) - .verifiable(typeMoq.Times.atLeastOnce()); - workspaceService.setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder.object, workspaceFolder.object]) - .verifiable(typeMoq.Times.atLeastOnce()); - workspaceService.setup(w => w.getWorkspaceFolder(typeMoq.It.isValue(workspaceUri))) - .returns(() => workspaceFolder.object) - .verifiable(typeMoq.Times.once()); - - const pythonConfig = typeMoq.Mock.ofType(); - workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri))) - .returns(() => pythonConfig.object) - .verifiable(typeMoq.Times.once()); - - const { configValue, configName } = getExpectedValueAndSettings(); - pythonConfig.setup(p => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - - if (updateMethod === 'updateTestArgs') { - await testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); - } else { - await testConfigSettingsService[updateMethod](workspaceUri, product); - } - - workspaceService.verifyAll(); - pythonConfig.verifyAll(); - }); - test('Expect an exception when updating Test Arguments with workspace Uri with more than one workspace and uri does not belong to a workspace', async () => { - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(typeMoq.Times.atLeastOnce()); - - const workspaceFolder = typeMoq.Mock.ofType(); - workspaceFolder.setup(w => w.uri) - .returns(() => workspaceUri) - .verifiable(typeMoq.Times.atLeastOnce()); - workspaceService.setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder.object, workspaceFolder.object]) - .verifiable(typeMoq.Times.atLeastOnce()); - workspaceService.setup(w => w.getWorkspaceFolder(typeMoq.It.isValue(workspaceUri))) - .returns(() => undefined) - .verifiable(typeMoq.Times.once()); - - const { configValue } = getExpectedValueAndSettings(); - - const promise = testConfigSettingsService.updateTestArgs(workspaceUri, product, configValue); - expect(promise).to.eventually.rejectedWith(); - workspaceService.verifyAll(); - }); - }); - }); - }); -}); diff --git a/src/test/unittests/configuration.unit.test.ts b/src/test/unittests/configuration.unit.test.ts deleted file mode 100644 index cc89f4bfa76c..000000000000 --- a/src/test/unittests/configuration.unit.test.ts +++ /dev/null @@ -1,340 +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 { OutputChannel, Uri, WorkspaceConfiguration } from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; -import { IConfigurationService, IInstaller, IOutputChannel, IPythonSettings, IUnitTestSettings, Product } from '../../client/common/types'; -import { getNamesAndValues } from '../../client/common/utils/enum'; -import { IServiceContainer } from '../../client/ioc/types'; -import { TEST_OUTPUT_CHANNEL } from '../../client/unittests/common/constants'; -import { UnitTestProduct } from '../../client/unittests/common/types'; -import { UnitTestConfigurationService } from '../../client/unittests/configuration'; -import { ITestConfigurationManager, ITestConfigurationManagerFactory } from '../../client/unittests/types'; - -suite('Unit Tests - ConfigurationService', () => { - [Product.pytest, Product.unittest, Product.nosetest].forEach(prodItem => { - const product = prodItem as any as UnitTestProduct; - const prods = getNamesAndValues(Product); - const productName = prods.filter(item => item.value === product)[0]; - const workspaceUri = Uri.file(__filename); - suite(productName.name, () => { - let testConfigService: typeMoq.IMock; - let workspaceService: typeMoq.IMock; - let factory: typeMoq.IMock; - let appShell: typeMoq.IMock; - let unitTestSettings: typeMoq.IMock; - setup(() => { - const serviceContainer = typeMoq.Mock.ofType(); - const configurationService = typeMoq.Mock.ofType(); - appShell = typeMoq.Mock.ofType(); - const outputChannel = typeMoq.Mock.ofType(); - const installer = typeMoq.Mock.ofType(); - workspaceService = typeMoq.Mock.ofType(); - factory = typeMoq.Mock.ofType(); - unitTestSettings = typeMoq.Mock.ofType(); - const pythonSettings = typeMoq.Mock.ofType(); - - pythonSettings.setup(p => p.unitTest).returns(() => unitTestSettings.object); - configurationService.setup(c => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); - - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))).returns(() => outputChannel.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IConfigurationService))).returns(() => configurationService.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(ITestConfigurationManagerFactory))).returns(() => factory.object); - testConfigService = typeMoq.Mock.ofType(UnitTestConfigurationService, typeMoq.MockBehavior.Loose, true, serviceContainer.object); - }); - test('Enable Test when setting unitTest.promptToConfigure is enabled', async () => { - const configMgr = typeMoq.Mock.ofType(); - configMgr.setup(c => c.enable()) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - - factory.setup(f => f.create(workspaceUri, product)) - .returns(() => configMgr.object) - .verifiable(typeMoq.Times.once()); - - const workspaceConfig = typeMoq.Mock.ofType(); - workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) - .returns(() => workspaceConfig.object) - .verifiable(typeMoq.Times.once()); - - workspaceConfig.setup(w => w.get(typeMoq.It.isValue('unitTest.promptToConfigure'))) - .returns(() => true) - .verifiable(typeMoq.Times.once()); - - await testConfigService.target.enableTest(workspaceUri, product); - - configMgr.verifyAll(); - factory.verifyAll(); - workspaceService.verifyAll(); - workspaceConfig.verifyAll(); - }); - test('Enable Test when setting unitTest.promptToConfigure is disabled', async () => { - const configMgr = typeMoq.Mock.ofType(); - configMgr.setup(c => c.enable()) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - - factory.setup(f => f.create(workspaceUri, product)) - .returns(() => configMgr.object) - .verifiable(typeMoq.Times.once()); - - const workspaceConfig = typeMoq.Mock.ofType(); - workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) - .returns(() => workspaceConfig.object) - .verifiable(typeMoq.Times.once()); - - workspaceConfig.setup(w => w.get(typeMoq.It.isValue('unitTest.promptToConfigure'))) - .returns(() => false) - .verifiable(typeMoq.Times.once()); - - workspaceConfig.setup(w => w.update(typeMoq.It.isValue('unitTest.promptToConfigure'), typeMoq.It.isValue(undefined))) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - - await testConfigService.target.enableTest(workspaceUri, product); - - configMgr.verifyAll(); - factory.verifyAll(); - workspaceService.verifyAll(); - workspaceConfig.verifyAll(); - }); - test('Enable Test when setting unitTest.promptToConfigure is disabled and fail to update the settings', async () => { - const configMgr = typeMoq.Mock.ofType(); - configMgr.setup(c => c.enable()) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - - factory.setup(f => f.create(workspaceUri, product)) - .returns(() => configMgr.object) - .verifiable(typeMoq.Times.once()); - - const workspaceConfig = typeMoq.Mock.ofType(); - workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) - .returns(() => workspaceConfig.object) - .verifiable(typeMoq.Times.once()); - - workspaceConfig.setup(w => w.get(typeMoq.It.isValue('unitTest.promptToConfigure'))) - .returns(() => false) - .verifiable(typeMoq.Times.once()); - - const errorMessage = 'Update Failed'; - const updateFailError = new Error(errorMessage); - workspaceConfig.setup(w => w.update(typeMoq.It.isValue('unitTest.promptToConfigure'), typeMoq.It.isValue(undefined))) - .returns(() => Promise.reject(updateFailError)) - .verifiable(typeMoq.Times.once()); - - const promise = testConfigService.target.enableTest(workspaceUri, product); - - await expect(promise).to.eventually.be.rejectedWith(errorMessage); - configMgr.verifyAll(); - factory.verifyAll(); - workspaceService.verifyAll(); - workspaceConfig.verifyAll(); - }); - test('Select Test runner displays 3 items', async () => { - const placeHolder = 'Some message'; - appShell.setup(s => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) - .callback(items => expect(items).be.lengthOf(3)) - .verifiable(typeMoq.Times.once()); - - await testConfigService.target.selectTestRunner(placeHolder); - appShell.verifyAll(); - }); - test('Ensure selected item is returned', async () => { - const placeHolder = 'Some message'; - const indexes = [Product.unittest, Product.pytest, Product.nosetest]; - appShell.setup(s => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) - .callback(items => expect(items).be.lengthOf(3)) - .returns((items) => items[indexes.indexOf(product)]) - .verifiable(typeMoq.Times.once()); - - const selectedItem = await testConfigService.target.selectTestRunner(placeHolder); - expect(selectedItem).to.be.equal(product); - appShell.verifyAll(); - }); - test('Ensure undefined is returned when nothing is seleted', async () => { - const placeHolder = 'Some message'; - appShell.setup(s => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) - .returns(() => Promise.resolve(undefined)) - .verifiable(typeMoq.Times.once()); - - const selectedItem = await testConfigService.target.selectTestRunner(placeHolder); - expect(selectedItem).to.be.equal(undefined, 'invalid value'); - appShell.verifyAll(); - }); - test('Prompt to enable a test if a test framework is not enabled', async () => { - unitTestSettings.setup(u => u.pyTestEnabled).returns(() => false); - unitTestSettings.setup(u => u.unittestEnabled).returns(() => false); - unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => false); - - appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typeMoq.Times.once()); - - let exceptionThrown = false; - try { - await testConfigService.target.displayTestFrameworkError(workspaceUri); - } catch { - exceptionThrown = true; - } - - expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); - appShell.verifyAll(); - }); - test('Prompt to select a test if a test framework is not enabled', async () => { - unitTestSettings.setup(u => u.pyTestEnabled).returns(() => false); - unitTestSettings.setup(u => u.unittestEnabled).returns(() => false); - unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => false); - - appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((_msg, option) => Promise.resolve(option)) - .verifiable(typeMoq.Times.once()); - - let exceptionThrown = false; - let selectTestRunnerInvoked = false; - try { - testConfigService.callBase = false; - testConfigService.setup(t => t.selectTestRunner(typeMoq.It.isAny())) - .returns(() => { - selectTestRunnerInvoked = true; - return Promise.resolve(undefined); - }); - await testConfigService.target.displayTestFrameworkError(workspaceUri); - } catch { - 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); - - 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); - }); - - 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(); - factory.setup(f => f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product))) - .returns(() => configMgr.object) - .verifiable(typeMoq.Times.once()); - - configMgr.setup(c => c.configure(typeMoq.It.isValue(workspaceUri))) - .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(true, 'Enable Test not invoked'); - appShell.verifyAll(); - factory.verifyAll(); - configMgr.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()); - - let exceptionThrown = false; - try { - await testConfigService.target.displayTestFrameworkError(workspaceUri); - } catch { - 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(); - factory.setup(f => f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product))) - .returns(() => configMgr.object) - .verifiable(typeMoq.Times.once()); - - configMgr.setup(c => c.configure(typeMoq.It.isValue(workspaceUri))) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.never()); - const configManagersToVerify: typeof configMgr[] = [configMgr]; - - [Product.unittest, Product.pytest, Product.nosetest] - .filter(prod => product !== prod) - .forEach(prod => { - const otherTestConfigMgr = typeMoq.Mock.ofType(); - factory.setup(f => f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(prod))) - .returns(() => otherTestConfigMgr.object) - .verifiable(typeMoq.Times.once()); - otherTestConfigMgr.setup(c => c.disable()) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - - configManagersToVerify.push(otherTestConfigMgr); - }); - - 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(); - for (const item of configManagersToVerify) { - item.verifyAll(); - } - }); - }); - }); -}); diff --git a/src/test/unittests/configurationFactory.unit.test.ts b/src/test/unittests/configurationFactory.unit.test.ts deleted file mode 100644 index 23e316f4ef55..000000000000 --- a/src/test/unittests/configurationFactory.unit.test.ts +++ /dev/null @@ -1,47 +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 typeMoq from 'typemoq'; -import { OutputChannel, Uri } from 'vscode'; -import { IInstaller, IOutputChannel, Product } from '../../client/common/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { TEST_OUTPUT_CHANNEL } from '../../client/unittests/common/constants'; -import { ITestConfigSettingsService } from '../../client/unittests/common/types'; -import { TestConfigurationManagerFactory } from '../../client/unittests/configurationFactory'; -import * as nose from '../../client/unittests/nosetest/testConfigurationManager'; -import * as pytest from '../../client/unittests/pytest/testConfigurationManager'; -import { ITestConfigurationManagerFactory } from '../../client/unittests/types'; -import * as unittest from '../../client/unittests/unittest/testConfigurationManager'; - -use(chaiAsPromised); - -suite('Unit Tests - ConfigurationManagerFactory', () => { - let factory: ITestConfigurationManagerFactory; - setup(() => { - const serviceContainer = typeMoq.Mock.ofType(); - const outputChannel = typeMoq.Mock.ofType(); - const installer = typeMoq.Mock.ofType(); - const testConfigService = typeMoq.Mock.ofType(); - - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))).returns(() => outputChannel.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(ITestConfigSettingsService))).returns(() => testConfigService.object); - factory = new TestConfigurationManagerFactory(serviceContainer.object); - }); - test('Create Unit Test Configuration', async () => { - const configMgr = factory.create(Uri.file(__filename), Product.unittest); - expect(configMgr).to.be.instanceOf(unittest.ConfigurationManager); - }); - test('Create pytest Configuration', async () => { - const configMgr = factory.create(Uri.file(__filename), Product.pytest); - expect(configMgr).to.be.instanceOf(pytest.ConfigurationManager); - }); - test('Create nose Configuration', async () => { - const configMgr = factory.create(Uri.file(__filename), Product.nosetest); - expect(configMgr).to.be.instanceOf(nose.ConfigurationManager); - }); -}); diff --git a/src/test/unittests/debugger.test.ts b/src/test/unittests/debugger.test.ts deleted file mode 100644 index c55792c519ab..000000000000 --- a/src/test/unittests/debugger.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { assert, expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as path from 'path'; -import { ConfigurationTarget } from 'vscode'; -import { createDeferred } from '../../client/common/utils/async'; -import { TestManagerRunner as NoseTestManagerRunner } from '../../client/unittests//nosetest/runner'; -import { TestManagerRunner as PytestManagerRunner } from '../../client/unittests//pytest/runner'; -import { TestManagerRunner as UnitTestTestManagerRunner } from '../../client/unittests//unittest/runner'; -import { ArgumentsHelper } from '../../client/unittests/common/argumentsHelper'; -import { CANCELLATION_REASON, CommandSource, NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../client/unittests/common/constants'; -import { TestRunner } from '../../client/unittests/common/runner'; -import { ITestDebugLauncher, ITestManagerFactory, ITestMessageService, ITestRunner, IXUnitParser, TestProvider } from '../../client/unittests/common/types'; -import { XUnitParser } from '../../client/unittests/common/xUnitParser'; -import { ArgumentsService as NoseTestArgumentsService } from '../../client/unittests/nosetest/services/argsService'; -import { ArgumentsService as PyTestArgumentsService } from '../../client/unittests/pytest/services/argsService'; -import { TestMessageService } from '../../client/unittests/pytest/services/testMessageService'; -import { IArgumentsHelper, IArgumentsService, ITestManagerRunner, IUnitTestHelper } from '../../client/unittests/types'; -import { UnitTestHelper } from '../../client/unittests/unittest/helper'; -import { ArgumentsService as UnitTestArgumentsService } from '../../client/unittests/unittest/services/argsService'; -import { deleteDirectory, rootWorkspaceUri, updateSetting } from '../common'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } 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 () => { - // Test disvovery is where the delay is, hence give 10 seconds (as we discover tests at least twice in each test). - await initialize(); - await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); - await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); - await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); - }); - setup(async () => { - await deleteDirectory(path.join(testFilesPath, '.cache')); - await initializeTest(); - initializeDI(); - }); - teardown(async () => { - await ioc.dispose(); - await updateSetting('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); - await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); - await updateSetting('unitTest.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.serviceManager.add(IArgumentsHelper, ArgumentsHelper); - ioc.serviceManager.add(ITestRunner, TestRunner); - ioc.serviceManager.add(IXUnitParser, XUnitParser); - ioc.serviceManager.add(IUnitTestHelper, UnitTestHelper); - ioc.serviceManager.add(IArgumentsService, NoseTestArgumentsService, NOSETEST_PROVIDER); - ioc.serviceManager.add(IArgumentsService, PyTestArgumentsService, PYTEST_PROVIDER); - ioc.serviceManager.add(IArgumentsService, UnitTestArgumentsService, UNITTEST_PROVIDER); - ioc.serviceManager.add(ITestManagerRunner, PytestManagerRunner, PYTEST_PROVIDER); - ioc.serviceManager.add(ITestManagerRunner, NoseTestManagerRunner, NOSETEST_PROVIDER); - ioc.serviceManager.add(ITestManagerRunner, UnitTestTestManagerRunner, UNITTEST_PROVIDER); - ioc.serviceManager.addSingleton(ITestDebugLauncher, MockDebugLauncher); - ioc.serviceManager.addSingleton(ITestMessageService, TestMessageService, PYTEST_PROVIDER); - } - - async function testStartingDebugger(testProvider: TestProvider) { - const testManager = ioc.serviceContainer.get(ITestManagerFactory)(testProvider, rootWorkspaceUri!, testFilesPath); - const mockDebugLauncher = ioc.serviceContainer.get(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(); - 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('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); - await testStartingDebugger('unittest'); - }); - - test('Debugger should start (pytest)', async () => { - await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); - await testStartingDebugger('pytest'); - }); - - test('Debugger should start (nosetest)', async () => { - await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - await testStartingDebugger('nosetest'); - }); - - async function testStoppingDebugger(testProvider: TestProvider) { - const testManager = ioc.serviceContainer.get(ITestManagerFactory)(testProvider, rootWorkspaceUri!, testFilesPath); - const mockDebugLauncher = ioc.serviceContainer.get(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('unitTest.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('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); - await testStoppingDebugger('pytest'); - }); - - test('Debugger should stop when user invokes a test discovery (nosetest)', async () => { - await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - await testStoppingDebugger('nosetest'); - }); - - async function testDebuggerWhenRediscoveringTests(testProvider: TestProvider) { - const testManager = ioc.serviceContainer.get(ITestManagerFactory)(testProvider, rootWorkspaceUri!, testFilesPath); - const mockDebugLauncher = ioc.serviceContainer.get(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(); - - 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('unitTest.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('unitTest.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('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - await testDebuggerWhenRediscoveringTests('nosetest'); - }); -}); diff --git a/src/test/unittests/display/main.test.ts b/src/test/unittests/display/main.test.ts deleted file mode 100644 index fa5e8ac124ec..000000000000 --- a/src/test/unittests/display/main.test.ts +++ /dev/null @@ -1,366 +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 } from '../../../client/common/application/types'; -import { Commands } from '../../../client/common/constants'; -import { IConfigurationService, IPythonSettings, IUnitTestSettings } from '../../../client/common/types'; -import { createDeferred } from '../../../client/common/utils/async'; -import { noop } from '../../../client/common/utils/misc'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { CANCELLATION_REASON } from '../../../client/unittests/common/constants'; -import { ITestsHelper, Tests } from '../../../client/unittests/common/types'; -import { TestResultDisplay } from '../../../client/unittests/display/main'; -import { sleep } from '../../core'; - -suite('Unit Tests - TestResultDisplay', () => { - const workspaceUri = Uri.file(__filename); - let appShell: typeMoq.IMock; - let unitTestSettings: typeMoq.IMock; - let serviceContainer: typeMoq.IMock; - let display: TestResultDisplay; - let testsHelper: typeMoq.IMock; - let configurationService: typeMoq.IMock; - setup(() => { - serviceContainer = typeMoq.Mock.ofType(); - configurationService = typeMoq.Mock.ofType(); - appShell = typeMoq.Mock.ofType(); - unitTestSettings = typeMoq.Mock.ofType(); - const pythonSettings = typeMoq.Mock.ofType(); - testsHelper = typeMoq.Mock.ofType(); - - pythonSettings.setup(p => p.unitTest).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); - }); - 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(); - 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(); - 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(); - 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(); - 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(); - 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().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(); - 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(); - - 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.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(); - 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(); - - 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.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(); - 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(); - - 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(); - 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(); - - 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(); - 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().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(); - 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(); - - 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(); - 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(); - 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(); - - 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(); - appShell.setup(a => a.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((msg, item) => Promise.resolve(item)) - .verifiable(typeMoq.Times.once()); - - for (const setting of ['unitTest.promptToConfigure', 'unitTest.pyTestEnabled', - 'unitTest.unittestEnabled', 'unitTest.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(); - }); - 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(); - 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(); - - 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(); - 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(); - - 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/unittests/helper.ts b/src/test/unittests/helper.ts deleted file mode 100644 index 8f480665cf1b..000000000000 --- a/src/test/unittests/helper.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as assert from 'assert'; -import { sep } from 'path'; -import { IS_WINDOWS } from '../../client/common/platform/constants'; -import { Tests } from '../../client/unittests/common/types'; - -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}'`); -} diff --git a/src/test/unittests/mocks.ts b/src/test/unittests/mocks.ts deleted file mode 100644 index 5d837dedfe76..000000000000 --- a/src/test/unittests/mocks.ts +++ /dev/null @@ -1,117 +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/unittests/common/constants'; -import { BaseTestManager } from '../../client/unittests/common/managers/baseTestManager'; -import { ITestDebugLauncher, ITestDiscoveryService, IUnitTestSocketServer, LaunchOptions, TestDiscoveryOptions, TestProvider, Tests, TestsToRun } from '../../client/unittests/common/types'; - -@injectable() -export class MockDebugLauncher implements ITestDebugLauncher, Disposable { - public get launched(): Promise { - return this._launched.promise; - } - public get debuggerPromise(): Deferred { - // 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; - // tslint:disable-next-line:variable-name - private _promise?: Deferred; - // tslint:disable-next-line:variable-name - private _token?: CancellationToken; - constructor() { - this._launched = createDeferred(); - } - public async getLaunchOptions(resource?: Uri): Promise<{ port: number; host: string }> { - return { port: 0, host: 'localhost' }; - } - public async launchDebugger(options: LaunchOptions): Promise { - this._launched.resolve(true); - // tslint:disable-next-line:no-non-null-assertion - this._token = options.token!; - this._promise = createDeferred(); - // 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; - } - public dispose() { - this._promise = undefined; - } -} - -@injectable() -export class MockTestManagerWithRunningTests extends BaseTestManager { - // tslint:disable-next-line:no-any - public readonly runnerDeferred = createDeferred(); - public readonly enabled = true; - // tslint:disable-next-line:no-any - public readonly discoveryDeferred = createDeferred(); - 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 { - // 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 { - // 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) { } - public async discoverTests(options: TestDiscoveryOptions): Promise { - 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 { - 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/unittests/nosetest/nosetest.argsService.unit.test.ts b/src/test/unittests/nosetest/nosetest.argsService.unit.test.ts deleted file mode 100644 index 655834008b9f..000000000000 --- a/src/test/unittests/nosetest/nosetest.argsService.unit.test.ts +++ /dev/null @@ -1,51 +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 { ILogger } from '../../../client/common/types'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { ArgumentsHelper } from '../../../client/unittests/common/argumentsHelper'; -import { ArgumentsService as NoseTestArgumentsService } from '../../../client/unittests/nosetest/services/argsService'; -import { IArgumentsHelper } from '../../../client/unittests/types'; - -suite('ArgsService: nosetest', () => { - let argumentsService: NoseTestArgumentsService; - - suiteSetup(() => { - const serviceContainer = typeMoq.Mock.ofType(); - const logger = typeMoq.Mock.ofType(); - - serviceContainer - .setup(s => s.get(typeMoq.It.isValue(ILogger), typeMoq.It.isAny())) - .returns(() => logger.object); - - const argsHelper = new ArgumentsHelper(serviceContainer.object); - - 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 = ['anzy', '--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(2); - expect(testDirs[0]).to.equal(dir); - expect(testDirs[1]).to.equal(dir2); - }); -}); diff --git a/src/test/unittests/nosetest/nosetest.discovery.unit.test.ts b/src/test/unittests/nosetest/nosetest.discovery.unit.test.ts deleted file mode 100644 index 4ea9b9d54556..000000000000 --- a/src/test/unittests/nosetest/nosetest.discovery.unit.test.ts +++ /dev/null @@ -1,111 +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/unittests/common/constants'; -import { ITestDiscoveryService, ITestRunner, ITestsParser, Options, TestDiscoveryOptions, Tests } from '../../../client/unittests/common/types'; -import { TestDiscoveryService } from '../../../client/unittests/nosetest/services/discoveryService'; -import { IArgumentsService, TestFilter } from '../../../client/unittests/types'; - -use(chaipromise); - -suite('Unit Tests - nose - Discovery', () => { - let discoveryService: ITestDiscoveryService; - let argsService: typeMoq.IMock; - let testParser: typeMoq.IMock; - let runner: typeMoq.IMock; - setup(() => { - const serviceContainer = typeMoq.Mock.ofType(); - argsService = typeMoq.Mock.ofType(); - testParser = typeMoq.Mock.ofType(); - runner = typeMoq.Mock.ofType(); - - 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(); - const token = typeMoq.Mock.ofType(); - 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(); - const token = typeMoq.Mock.ofType(); - 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/unittests/nosetest/nosetest.disovery.test.ts b/src/test/unittests/nosetest/nosetest.disovery.test.ts deleted file mode 100644 index fbb5120e1868..000000000000 --- a/src/test/unittests/nosetest/nosetest.disovery.test.ts +++ /dev/null @@ -1,142 +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/unittests/common/constants'; -import { ITestManagerFactory } from '../../../client/unittests/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('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); - await initialize(); - }); - suiteTeardown(async () => { - await updateSetting('unitTest.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('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - - ioc.registerMockProcessTypes(); - } - - async function injectTestDiscoveryOutput(outputFileName: string) { - const procService = await ioc.serviceContainer.get(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); - 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); - 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('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get(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('unitTest.nosetestArgs', ['-w', 'specific', '-m', 'tst'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get(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('unitTest.nosetestArgs', ['-m', 'test_'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get(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/unittests/nosetest/nosetest.run.test.ts b/src/test/unittests/nosetest/nosetest.run.test.ts deleted file mode 100644 index 58d1464ef3c1..000000000000 --- a/src/test/unittests/nosetest/nosetest.run.test.ts +++ /dev/null @@ -1,179 +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/unittests/common/constants'; -import { ITestManagerFactory, TestsToRun } from '../../../client/unittests/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('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); - await initialize(); - }); - suiteTeardown(async () => { - await updateSetting('unitTest.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('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - - ioc.registerMockProcessTypes(); - } - - async function injectTestDiscoveryOutput(outputFileName: string) { - const procService = await ioc.serviceContainer.get(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).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('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get(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('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get(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('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get(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('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get(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('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get(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/unittests/nosetest/nosetest.test.ts b/src/test/unittests/nosetest/nosetest.test.ts deleted file mode 100644 index 4429186f92c9..000000000000 --- a/src/test/unittests/nosetest/nosetest.test.ts +++ /dev/null @@ -1,68 +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/unittests/common/constants'; -import { ITestManagerFactory } from '../../../client/unittests/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('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); - await initialize(); - }); - suiteTeardown(async () => { - await updateSetting('unitTest.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('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerProcessTypes(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - } - - test('Discover Tests (single test file)', async () => { - const factory = ioc.serviceContainer.get(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/unittests/pytest/pytest.argsService.unit.test.ts b/src/test/unittests/pytest/pytest.argsService.unit.test.ts deleted file mode 100644 index 105fd642e0b7..000000000000 --- a/src/test/unittests/pytest/pytest.argsService.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 } from 'chai'; -import * as path from 'path'; -import * as typeMoq from 'typemoq'; -import { ILogger } from '../../../client/common/types'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { ArgumentsHelper } from '../../../client/unittests/common/argumentsHelper'; -import { ArgumentsService as PyTestArgumentsService } from '../../../client/unittests/pytest/services/argsService'; -import { IArgumentsHelper } from '../../../client/unittests/types'; - -suite('ArgsService: pytest', () => { - let argumentsService: PyTestArgumentsService; - - suiteSetup(() => { - const serviceContainer = typeMoq.Mock.ofType(); - const logger = typeMoq.Mock.ofType(); - - serviceContainer - .setup(s => s.get(typeMoq.It.isValue(ILogger), typeMoq.It.isAny())) - .returns(() => logger.object); - - const argsHelper = new ArgumentsHelper(serviceContainer.object); - - 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 = ['anzy', '--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 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 = ['anzy', '--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 = ['anzy', '--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(2); - expect(testDirs[0]).to.equal(dir); - expect(testDirs[1]).to.equal(dir2); - }); -}); diff --git a/src/test/unittests/pytest/pytest.discovery.test.ts b/src/test/unittests/pytest/pytest.discovery.test.ts deleted file mode 100644 index 11ecbcbfa571..000000000000 --- a/src/test/unittests/pytest/pytest.discovery.test.ts +++ /dev/null @@ -1,294 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as assert from 'assert'; -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/unittests/common/constants'; -import { ITestManagerFactory } from '../../../client/unittests/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', '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', 'unitestsWithConfigs'); -const unitTestTestFilesCwdPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'cwd', 'src'); - -// tslint:disable-next-line: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; - suiteSetup(async () => { - await initialize(); - await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); - }); - setup(async () => { - await initializeTest(); - initializeDI(); - }); - teardown(async () => { - await ioc.dispose(); - await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - - // Mocks. - ioc.registerMockProcessTypes(); - } - - async function injectTestDiscoveryOutput(output: string) { - const procService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; - procService.onExecObservable((file, args, options, callback) => { - if (args.indexOf('--collect-only') >= 0) { - callback({ - out: output, - source: 'stdout' - }); - } - }); - } - - test('Discover Tests (single test file)', async () => { - // tslint:disable-next-line:no-multiline-string - await injectTestDiscoveryOutput(` - ============================= test session starts ============================== - platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 - rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single, inifile: - plugins: pylama-7.4.3 - collected 6 items - - - - - - - - - - - - ========================= no tests ran in 0.03 seconds ========================= - `); - const factory = ioc.serviceContainer.get(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 === 'tests/test_one.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); - }); - - test('Discover Tests (pattern = test_)', async () => { - // tslint:disable-next-line:no-multiline-string - await injectTestDiscoveryOutput(` - ============================= test session starts ============================== - platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 - rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard, inifile: - plugins: pylama-7.4.3 - collected 29 items - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ========================= no tests ran in 0.05 seconds ========================= - " - PROBLEMS - OUTPUT - DEBUG CONSOLE - TERMINAL - - - W - - Find - - `); - await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get(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, 6, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 29, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 8, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_unittest_one.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_unittest_two.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/unittest_three_test.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'tests/test_another_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); - }); - - test('Discover Tests (pattern = _test)', async () => { - // tslint:disable-next-line:no-multiline-string - await injectTestDiscoveryOutput(` - ============================= test session starts ============================== - platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 - rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard, inifile: - plugins: pylama-7.4.3 - collected 29 items - - - - - - ============================= 27 tests deselected ============================== - ======================== 27 deselected in 0.05 seconds ========================= - `); - await updateSetting('unitTest.pyTestArgs', ['-k=_test.py'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get(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 === 'tests/unittest_three_test.py' && t.nameToRun === t.name), true, 'Test File not found'); - }); - - test('Discover Tests (with config)', async () => { - // tslint:disable-next-line:no-multiline-string - await injectTestDiscoveryOutput(` - ============================= test session starts ============================== - platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 - rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/unitestsWithConfigs, inifile: pytest.ini - plugins: pylama-7.4.3 - collected 14 items - - - - - - - - - - - - - - - - - - - - - - - - - ========================= no tests ran in 0.04 seconds ========================= - `); - await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get(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 === 'other/test_unittest_one.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'other/test_pytest.py' && t.nameToRun === t.name), true, 'Test File not found'); - }); - - test('Setting cwd should return tests', async () => { - // tslint:disable-next-line:no-multiline-string - await injectTestDiscoveryOutput(` - ============================= test session starts ============================== - platform darwin -- Python 3.6.2, pytest-3.3.0, py-1.5.2, pluggy-0.6.0 - rootdir: /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/cwd/src, inifile: - plugins: pylama-7.4.3 - collected 1 item - - - - - ========================= no tests ran in 0.02 seconds ========================= - `); - await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get(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, 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/unittests/pytest/pytest.discovery.unit.test.ts b/src/test/unittests/pytest/pytest.discovery.unit.test.ts deleted file mode 100644 index 09a55d9bd91c..000000000000 --- a/src/test/unittests/pytest/pytest.discovery.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: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 } from 'vscode'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { PYTEST_PROVIDER } from '../../../client/unittests/common/constants'; -import { - ITestDiscoveryService, ITestRunner, ITestsHelper, - ITestsParser, Options, TestDiscoveryOptions, Tests -} from '../../../client/unittests/common/types'; -import { TestDiscoveryService } from '../../../client/unittests/pytest/services/discoveryService'; -import { IArgumentsService, TestFilter } from '../../../client/unittests/types'; - -use(chaipromise); - -suite('Unit Tests - PyTest - Discovery', () => { - let discoveryService: ITestDiscoveryService; - let argsService: typeMoq.IMock; - let testParser: typeMoq.IMock; - let runner: typeMoq.IMock; - let helper: typeMoq.IMock; - - setup(() => { - const serviceContainer = typeMoq.Mock.ofType(); - argsService = typeMoq.Mock.ofType(); - testParser = typeMoq.Mock.ofType(); - runner = typeMoq.Mock.ofType(); - helper = typeMoq.Mock.ofType(); - - 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); - serviceContainer.setup(s => s.get(typeMoq.It.isValue(ITestsHelper), typeMoq.It.isAny())) - .returns(() => helper.object); - - discoveryService = new TestDiscoveryService(serviceContainer.object, testParser.object); - }); - test('Ensure discovery is invoked with the right args and single dir', async () => { - const args: string[] = []; - const runOutput = 'xyz'; - const dir = path.join('a', 'b', 'c'); - 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()); - argsService.setup(a => a.getTestFolders(typeMoq.It.isValue(args))) - .returns(() => [dir]) - .verifiable(typeMoq.Times.once()); - helper.setup(a => a.mergeTests(typeMoq.It.isAny())) - .returns(() => tests) - .verifiable(typeMoq.Times.once()); - runner.setup(r => r.run(typeMoq.It.isValue(PYTEST_PROVIDER), typeMoq.It.isAny())) - .callback((_, opts: Options) => { - expect(opts.args).to.include('--cache-clear'); - expect(opts.args).to.include('-s'); - expect(opts.args).to.include('--collect-only'); - expect(opts.args[opts.args.length - 1]).to.equal(dir); - }) - .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(); - const token = typeMoq.Mock.ofType(); - 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(); - helper.verifyAll(); - }); - test('Ensure discovery is invoked with the right args and multiple dirs', async () => { - const args: string[] = []; - const runOutput = 'xyz'; - const dirs = [path.join('a', 'b', '1'), path.join('a', 'b', '2')]; - 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()); - argsService.setup(a => a.getTestFolders(typeMoq.It.isValue(args))) - .returns(() => dirs) - .verifiable(typeMoq.Times.once()); - helper.setup(a => a.mergeTests(typeMoq.It.isAny())) - .returns(() => tests) - .verifiable(typeMoq.Times.once()); - runner.setup(r => r.run(typeMoq.It.isValue(PYTEST_PROVIDER), typeMoq.It.isAny())) - .callback((_, opts: Options) => { - expect(opts.args).to.include('--cache-clear'); - expect(opts.args).to.include('-s'); - expect(opts.args).to.include('--collect-only'); - const dir = opts.args[opts.args.length - 1]; - expect(dirs).to.include(dir); - dirs.splice(dirs.indexOf(dir) - 1, 1); - }) - .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(); - const token = typeMoq.Mock.ofType(); - 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(); - helper.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()); - argsService.setup(a => a.getTestFolders(typeMoq.It.isValue(args))) - .returns(() => ['']) - .verifiable(typeMoq.Times.once()); - runner.setup(r => r.run(typeMoq.It.isValue(PYTEST_PROVIDER), typeMoq.It.isAny())) - .callback((_, opts: Options) => { - expect(opts.args).to.include('--cache-clear'); - expect(opts.args).to.include('-s'); - expect(opts.args).to.include('--collect-only'); - }) - .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()); - helper.setup(a => a.mergeTests(typeMoq.It.isAny())) - .returns(() => tests); - - const options = typeMoq.Mock.ofType(); - const token = typeMoq.Mock.ofType(); - 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/unittests/pytest/pytest.run.test.ts b/src/test/unittests/pytest/pytest.run.test.ts deleted file mode 100644 index e15a0f98a438..000000000000 --- a/src/test/unittests/pytest/pytest.run.test.ts +++ /dev/null @@ -1,446 +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 { IFileSystem } from '../../../client/common/platform/types'; -import { IProcessServiceFactory } from '../../../client/common/process/types'; -import { CommandSource } from '../../../client/unittests/common/constants'; -import { UnitTestDiagnosticService } from '../../../client/unittests/common/services/unitTestDiagnosticService'; -import { FlattenedTestFunction, ITestManager, ITestManagerFactory, Tests, TestStatus, TestsToRun } from '../../../client/unittests/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'; -import { ITestDetails, ITestScenarioDetails, testScenarios } from './pytest_run_tests_data'; - -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 { - 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 { - 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: 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((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 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); - 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 { - 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'); -} - -// tslint:disable-next-line:max-func-body-length -suite('Unit Tests - pytest - run with mocked process output', () => { - let ioc: UnitTestIocContainer; - const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; - suiteSetup(async () => { - await initialize(); - await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - // Mocks. - ioc.registerMockProcessTypes(); - } - - async function injectTestDiscoveryOutput(outputFileName: string) { - const procService = await ioc.serviceContainer.get(IProcessServiceFactory).create() as MockProcessService; - procService.onExecObservable((file, args, options, callback) => { - if (args.indexOf('--collect-only') >= 0) { - callback({ - out: fs.readFileSync(path.join(PYTEST_RESULTS_PATH, outputFileName), 'utf8').replace(/\/Users\/donjayamanne\/.vscode\/extensions\/pythonVSCode\/src\/test\/pythonFiles\/testFiles\/noseFiles/g, PYTEST_RESULTS_PATH), - source: 'stdout' - }); - } - }); - } - async function injectTestRunOutput(outputFileName: string, failedOutput: boolean = false) { - const procService = await ioc.serviceContainer.get(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('--junitxml=')); - if (index >= 0) { - const fileName = args[index].substr('--junitxml='.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: vscode.Diagnostic[]; - suiteSetup(async () => { - await initializeTest(); - initializeDI(); - await injectTestDiscoveryOutput(scenario.discoveryOutput); - await injectTestRunOutput(scenario.runOutput); - if (scenario.shouldRunFailed === true) { await injectTestRunOutput(scenario.failedRunOutput!, true); } - await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); - factory = ioc.serviceContainer.get(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('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); - }); - const shouldRunProperly = (suiteName, failedRun = false) => { - suite(suiteName, () => { - testDetails = getScenarioTestDetails(scenario, failedRun); - const uniqueIssueFiles = getUniqueIssueFilesFromTestDetails(testDetails); - let expectedSummaryCount; - 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; - let expectedDiagnosticCount: number; - 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)!; - expectedDiagnosticCount = 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/unittests/pytest/pytest.test.ts b/src/test/unittests/pytest/pytest.test.ts deleted file mode 100644 index 5ea6e1a36627..000000000000 --- a/src/test/unittests/pytest/pytest.test.ts +++ /dev/null @@ -1,48 +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/unittests/common/constants'; -import { ITestManagerFactory } from '../../../client/unittests/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('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); - }); - setup(async () => { - await initializeTest(); - initializeDI(); - }); - teardown(async () => { - await ioc.dispose(); - await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerProcessTypes(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - } - - test('Discover Tests (single test file)', async () => { - const factory = ioc.serviceContainer.get(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 === 'tests/test_one.py' && t.nameToRun === t.name), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_root.py' && t.nameToRun === t.name), true, 'Test File not found'); - }); -}); diff --git a/src/test/unittests/pytest/pytest.testMessageService.test.ts b/src/test/unittests/pytest/pytest.testMessageService.test.ts deleted file mode 100644 index d869fba7076d..000000000000 --- a/src/test/unittests/pytest/pytest.testMessageService.test.ts +++ /dev/null @@ -1,182 +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 * as typeMoq from 'typemoq'; -import * as vscode from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { ProductNames } from '../../../client/common/installer/productNames'; -import { Product } from '../../../client/common/types'; -import { TestResultsService } from '../../../client/unittests/common/services/testResultsService'; -import { TestsHelper } from '../../../client/unittests/common/testUtils'; -import { TestFlatteningVisitor } from '../../../client/unittests/common/testVisitors/flatteningVisitor'; -import { ITestVisitor, PassCalculationFormulae, TestDiscoveryOptions, Tests, TestStatus } from '../../../client/unittests/common/types'; -import { XUnitParser } from '../../../client/unittests/common/xUnitParser'; -import { TestsParser as PyTestsParser } from '../../../client/unittests/pytest/services/parserService'; -import { TestMessageService } from '../../../client/unittests/pytest/services/testMessageService'; -import { ILocationStackFrameDetails, IPythonUnitTestMessage, PythonUnitTestMessageSeverity } from '../../../client/unittests/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'; - -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: IPythonUnitTestMessage, expectedMessage: IPythonUnitTestMessage, imported: boolean = false, status: TestStatus) { - assert.equal(message.code, expectedMessage.code, 'IPythonUnitTestMessage code'); - assert.equal(message.message, expectedMessage.message, 'IPythonUnitTestMessage message'); - assert.equal(message.severity, expectedMessage.severity, 'IPythonUnitTestMessage severity'); - assert.equal(message.provider, expectedMessage.provider, 'IPythonUnitTestMessage provider'); - assert.isNumber(message.testTime, 'IPythonUnitTestMessage testTime'); - assert.equal(message.status, expectedMessage.status, 'IPythonUnitTestMessage status'); - assert.equal(message.testFilePath, expectedMessage.testFilePath, 'IPythonUnitTestMessage testFilePath'); - if (status !== TestStatus.Pass) { - assert.equal(message.locationStack![0].lineText, expectedMessage.locationStack![0].lineText, 'IPythonUnitTestMessage line text'); - assert.equal(message.locationStack![0].location.uri.fsPath, expectedMessage.locationStack![0].location.uri.fsPath, 'IPythonUnitTestMessage locationStack fsPath'); - if (status !== TestStatus.Skipped) { - assert.equal(message.locationStack![1].lineText, expectedMessage.locationStack![1].lineText, 'IPythonUnitTestMessage line text'); - assert.equal(message.locationStack![1].location.uri.fsPath, expectedMessage.locationStack![1].location.uri.fsPath, 'IPythonUnitTestMessage locationStack fsPath'); - } - if (imported) { - assert.equal(message.locationStack![2].lineText, expectedMessage.locationStack![2].lineText, 'IPythonUnitTestMessage imported line text'); - assert.equal(message.locationStack![2].location.uri.fsPath, expectedMessage.locationStack![2].location.uri.fsPath, 'IPythonUnitTestMessage 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 { - 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; -} - -suite('Unit Tests - PyTest - TestMessageService', () => { - let ioc: UnitTestIocContainer; - const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; - suiteSetup(async () => { - await initialize(); - await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); - }); - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - // Mocks. - ioc.registerMockProcessTypes(); - } - // Build tests for the test data that is relevant for this platform. - filterdTestScenarios.forEach((scenario) => { - suite(scenario.scenarioName, async () => { - let testMessages: IPythonUnitTestMessage[]; - suiteSetup(async () => { - await initializeTest(); - initializeDI(); - // Setup the service container for use by the parser. - const testVisitor = typeMoq.Mock.ofType(); - const outChannel = typeMoq.Mock.ofType(); - const cancelToken = typeMoq.Mock.ofType(); - cancelToken.setup(c => c.isCancellationRequested).returns(() => false); - const wsFolder = typeMoq.Mock.ofType(); - const options: TestDiscoveryOptions = { - args: [], - cwd: UNITTEST_TEST_FILES_PATH, - ignoreCache: true, - outChannel: outChannel.object, - token: cancelToken.object, - workspaceFolder: wsFolder.object - }; - // Setup the parser. - const testFlattener: TestFlatteningVisitor = new TestFlatteningVisitor(); - const testHlp: TestsHelper = new TestsHelper(testFlattener, ioc.serviceContainer); - const parser = new PyTestsParser(testHlp); - const discoveryOutput = fs.readFileSync(path.join(PYTEST_RESULTS_PATH, scenario.discoveryOutput), 'utf8').replace(/\/Users\/donjayamanne\/.vscode\/extensions\/pythonVSCode\/src\/test\/pythonFiles\/testFiles\/noseFiles/g, PYTEST_RESULTS_PATH); - const parsedTests: Tests = parser.parse(discoveryOutput, options); - const xUnitParser = new XUnitParser(); - await xUnitParser.updateResultsFromXmlLogFile(parsedTests, path.join(PYTEST_RESULTS_PATH, scenario.runOutput), PassCalculationFormulae.pytest); - 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('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); - }); - scenario.testDetails!.forEach((td) => { - suite(td.nameToRun, () => { - let testMessage: IPythonUnitTestMessage; - let expectedMessage: IPythonUnitTestMessage; - suiteSetup(async () => { - let expectedSeverity: PythonUnitTestMessageSeverity; - if (td.status === TestStatus.Error || td.status === TestStatus.Fail) { - expectedSeverity = PythonUnitTestMessageSeverity.Error; - } else if (td.status === TestStatus.Skipped) { - expectedSeverity = PythonUnitTestMessageSeverity.Skip; - } else { - expectedSeverity = PythonUnitTestMessageSeverity.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/unittests/pytest/pytest.testparser.unit.test.ts b/src/test/unittests/pytest/pytest.testparser.unit.test.ts deleted file mode 100644 index dfef2b0a059c..000000000000 --- a/src/test/unittests/pytest/pytest.testparser.unit.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect, use } from 'chai'; -import * as chaipromise from 'chai-as-promised'; -import * as typeMoq from 'typemoq'; -import { CancellationToken, OutputChannel, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager } from '../../../client/common/application/types'; -import { OSType } from '../../../client/common/utils/platform'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { TestsHelper } from '../../../client/unittests/common/testUtils'; -import { TestFlatteningVisitor } from '../../../client/unittests/common/testVisitors/flatteningVisitor'; -import { FlattenedTestFunction, TestDiscoveryOptions, Tests } from '../../../client/unittests/common/types'; -import { TestsParser as PyTestsParser } from '../../../client/unittests/pytest/services/parserService'; -import { getOSType } from '../../common'; -import { PytestDataPlatformType, pytestScenarioData } from './pytest_unittest_parser_data'; - -use(chaipromise); - -// The PyTest test parsing is done via the stdout result of the -// `pytest --collect-only` command. -// -// There are a few limitations with this approach, the largest issue is mixing -// package and non-package style codebases (stdout does not give subdir -// information of tests in a package when __init__.py is not present). -// -// However, to test all of the various layouts that are available, we have -// created a JSON structure that defines all the tests - see file -// `pytest_unittest_parser_data.ts` in this folder. -suite('Unit Tests - PyTest - Test Parser used in discovery', () => { - - // Build tests for the test data that is relevant for this platform. - const testPlatformType: PytestDataPlatformType = - getOSType() === OSType.Windows ? - PytestDataPlatformType.Windows : PytestDataPlatformType.NonWindows; - - pytestScenarioData.forEach((testScenario) => { - if (testPlatformType === testScenario.platform) { - - const testDescription: string = - `PyTest${testScenario.pytest_version_spec}: ${testScenario.description}`; - - test(testDescription, async () => { - // Setup the service container for use by the parser. - const serviceContainer = typeMoq.Mock.ofType(); - const appShell = typeMoq.Mock.ofType(); - const cmdMgr = typeMoq.Mock.ofType(); - serviceContainer.setup(s => s.get(typeMoq.It.isValue(IApplicationShell), typeMoq.It.isAny())) - .returns(() => { - return appShell.object; - }); - serviceContainer.setup(s => s.get(typeMoq.It.isValue(ICommandManager), typeMoq.It.isAny())) - .returns(() => { - return cmdMgr.object; - }); - - // Create mocks used in the test discovery setup. - const outChannel = typeMoq.Mock.ofType(); - const cancelToken = typeMoq.Mock.ofType(); - cancelToken.setup(c => c.isCancellationRequested).returns(() => false); - const wsFolder = typeMoq.Mock.ofType(); - - // Create the test options for the mocked-up test. All data is either - // mocked or is taken from the JSON test data itself. - const options: TestDiscoveryOptions = { - args: [], - cwd: testScenario.rootdir, - ignoreCache: true, - outChannel: outChannel.object, - token: cancelToken.object, - workspaceFolder: wsFolder.object - }; - - // Setup the parser. - const testFlattener: TestFlatteningVisitor = new TestFlatteningVisitor(); - const testHlp: TestsHelper = new TestsHelper(testFlattener, serviceContainer.object); - const parser = new PyTestsParser(testHlp); - - // Each test scenario has a 'stdout' member that is an array of - // stdout lines. Join them here such that the parser can operate - // on stdout-like data. - const stdout: string = testScenario.stdout.join('\n'); - - const parsedTests: Tests = parser.parse(stdout, options); - - // Now we can actually perform tests. - expect(parsedTests).is.not.equal( - undefined, - 'Should have gotten tests extracted from the parsed pytest result content.'); - - expect(parsedTests.testFunctions.length).equals( - testScenario.functionCount, - `Parsed pytest summary contained ${testScenario.functionCount} test functions.`); - - testScenario.test_functions.forEach((funcName: string) => { - const findAllTests: FlattenedTestFunction[] | undefined = parsedTests.testFunctions.filter( - (tstFunc: FlattenedTestFunction) => { - return tstFunc.testFunction.nameToRun === funcName; - }); - // Each test identified in the testScenario should exist once and only once. - expect(findAllTests).is.not.equal(undefined, `Could not find "${funcName}" in tests.`); - expect(findAllTests.length).is.equal(1, 'There should be exactly one instance of each test.'); - }); - - }); - } - }); -}); diff --git a/src/test/unittests/pytest/pytest_run_tests_data.ts b/src/test/unittests/pytest/pytest_run_tests_data.ts deleted file mode 100644 index 3a483e8c5ae7..000000000000 --- a/src/test/unittests/pytest/pytest_run_tests_data.ts +++ /dev/null @@ -1,454 +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/unittests/common/types'; - -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, - 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, - 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, - testFunctionIndex: 0, - testDetails: allTestDetails.filter(td => {return td.testName === 'test_Root_A'; }) - }, - { - scenarioName: 'Run Failed Tests', - discoveryOutput: 'two.output', - runOutput: 'two.xml', - testsToRun: undefined, - testDetails: allTestDetails.filter(td => {return true; }), - shouldRunFailed: true, - failedRunOutput: 'two.again.xml' - } -]; diff --git a/src/test/unittests/pytest/pytest_unittest_parser_data.ts b/src/test/unittests/pytest/pytest_unittest_parser_data.ts deleted file mode 100644 index 73122e9e7f41..000000000000 --- a/src/test/unittests/pytest/pytest_unittest_parser_data.ts +++ /dev/null @@ -1,1387 +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", - "", - " ", - " ", - "", - " ", - "", - " ", - "", - " ", - "", - " ", - "", - " ", - " ", - "", - " ", - "", - "========================= no tests ran in 0.02 seconds =========================" - ] - }, - { - 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 9 items", - "", - " ", - " ", - "", - " ", - "", - " ", - "", - " ", - "", - " ", - "", - " ", - " ", - "", - " ", - "", - "========================= 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", - "", - " ", - " ", - "", - " ", - "", - " ", - "", - " ", - "", - "========================= no tests ran in 0.05 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 5 items", - "", - " ", - " ", - "", - " ", - "", - " ", - "", - " ", - "", - "========================= 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", - "", - " ", - " ", - "", - " ", - "", - " ", - "", - " ", - "", - "========================= no tests ran in 0.12 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 5 items", - "", - " ", - " ", - "", - " ", - "", - " ", - "", - " ", - "", - "========================= 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", - "", - " ", - "", - " ", - "", - "========================= no tests ran in 0.06 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 2 items", - "", - " ", - "", - " ", - "", - "========================= 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", - "", - " ", - "", - " ", - "", - "========================= 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 2 items", - "", - " ", - "", - " ", - "", - "========================= 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", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "========================= no tests ran in 0.07 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", - "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", - "", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "", - "========================= 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", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "========================= no tests ran in 0.18 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "", - "========================= 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", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "========================= no tests ran in 0.18 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_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", - "", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "", - "========================= 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", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "========================= no tests ran in 0.15 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "", - "========================= 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", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "========================= no tests ran in 0.23 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "", - "========================= 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", - "", - " ", - " ", - "", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== no tests ran in 0.30 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "", - "======================== 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", - "", - " ", - " ", - "", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== no tests ran in 0.11 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "", - " ", - " ", - "", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== 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", - "", - " ", - " ", - "", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== no tests ran in 0.26 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "", - "======================== 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", - "", - " ", - " ", - "", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== no tests ran in 0.17 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "", - " ", - " ", - "", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== 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", - "", - " ", - " ", - "", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== no tests ran in 0.26 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "", - "======================== 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", - "", - " ", - " ", - "", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== no tests ran in 0.11 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "", - " ", - " ", - "", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== 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", - "", - " ", - " ", - "", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== no tests ran in 0.20 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "", - "======================== 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", - "", - " ", - " ", - "", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "", - " ", - " ", - "", - " ", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== 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", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== no tests ran in 0.17 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 4 items", - "", - " ", - " ", - " ", - " ", - " ", - " ", - "", - "======================== 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", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== no tests ran in 0.18 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.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 4 items", - "", - " ", - " ", - "", - " ", - " ", - "", - "======================== no tests ran in 0.36 seconds =========================" - ] - } - ]; diff --git a/src/test/unittests/rediscover.test.ts b/src/test/unittests/rediscover.test.ts deleted file mode 100644 index 5e054111eb92..000000000000 --- a/src/test/unittests/rediscover.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { assert } from 'chai'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { ConfigurationTarget } from 'vscode'; -import { CommandSource } from '../../client/unittests/common/constants'; -import { ITestManagerFactory, TestProvider } from '../../client/unittests/common/types'; -import { deleteDirectory, deleteFile, rootWorkspaceUri, updateSetting } from '../common'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } 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 () => { - await fs.copy(testFileWithFewTests, testFile, { overwrite: true }); - await deleteDirectory(path.join(testFilesPath, '.cache')); - await resetSettings(); - await initializeTest(); - initializeDI(); - }); - teardown(async () => { - 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('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); - await updateSetting('unitTest.nosetestArgs', [], rootWorkspaceUri, configTarget); - await updateSetting('unitTest.pyTestArgs', [], rootWorkspaceUri, configTarget); - } - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerProcessTypes(); - ioc.registerVariableTypes(); - ioc.registerUnitTestTypes(); - } - - async function discoverUnitTests(testProvider: TestProvider) { - const testManager = ioc.serviceContainer.get(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('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); - await discoverUnitTests('unittest'); - }); - - test('Re-discover tests (pytest)', async () => { - await updateSetting('unitTest.pyTestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); - await discoverUnitTests('pytest'); - }); - - test('Re-discover tests (nosetest)', async () => { - await updateSetting('unitTest.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - await discoverUnitTests('nosetest'); - }); -}); diff --git a/src/test/unittests/serviceRegistry.ts b/src/test/unittests/serviceRegistry.ts deleted file mode 100644 index 1dee35313ad5..000000000000 --- a/src/test/unittests/serviceRegistry.ts +++ /dev/null @@ -1,155 +0,0 @@ -// 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 { History } from '../../client/datascience/history'; -import { HistoryProvider } from '../../client/datascience/historyProvider'; -import { JupyterExecution } from '../../client/datascience/jupyter/jupyterExecution'; -import { JupyterImporter } from '../../client/datascience/jupyter/jupyterImporter'; -import { JupyterServer } from '../../client/datascience/jupyter/jupyterServer'; -import { - ICodeCssGenerator, - IHistory, - IHistoryProvider, - IJupyterExecution, - INotebookImporter, - INotebookServer -} from '../../client/datascience/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../client/unittests/common/constants'; -import { TestCollectionStorageService } from '../../client/unittests/common/services/storageService'; -import { TestManagerService } from '../../client/unittests/common/services/testManagerService'; -import { TestResultsService } from '../../client/unittests/common/services/testResultsService'; -import { UnitTestDiagnosticService } from '../../client/unittests/common/services/unitTestDiagnosticService'; -import { TestsHelper } from '../../client/unittests/common/testUtils'; -import { TestFlatteningVisitor } from '../../client/unittests/common/testVisitors/flatteningVisitor'; -import { TestFolderGenerationVisitor } from '../../client/unittests/common/testVisitors/folderGenerationVisitor'; -import { TestResultResetVisitor } from '../../client/unittests/common/testVisitors/resultResetVisitor'; -import { - ITestCollectionStorageService, - ITestDiscoveryService, - ITestManager, - ITestManagerFactory, - ITestManagerService, - ITestManagerServiceFactory, - ITestResultsService, - ITestsHelper, - ITestsParser, - ITestVisitor, - IUnitTestSocketServer, - TestProvider -} from '../../client/unittests/common/types'; -import { TestManager as NoseTestManager } from '../../client/unittests/nosetest/main'; -import { TestDiscoveryService as NoseTestDiscoveryService } from '../../client/unittests/nosetest/services/discoveryService'; -import { TestsParser as NoseTestTestsParser } from '../../client/unittests/nosetest/services/parserService'; -import { TestManager as PyTestTestManager } from '../../client/unittests/pytest/main'; -import { TestDiscoveryService as PytestTestDiscoveryService } from '../../client/unittests/pytest/services/discoveryService'; -import { TestsParser as PytestTestsParser } from '../../client/unittests/pytest/services/parserService'; -import { IUnitTestDiagnosticService } from '../../client/unittests/types'; -import { TestManager as UnitTestTestManager } from '../../client/unittests/unittest/main'; -import { - TestDiscoveryService as UnitTestTestDiscoveryService -} from '../../client/unittests/unittest/services/discoveryService'; -import { TestsParser as UnitTestTestsParser } from '../../client/unittests/unittest/services/parserService'; -import { getPythonSemVer } from '../common'; -import { IocContainer } from '../serviceRegistry'; -import { MockUnitTestSocketServer } from './mocks'; - -export class UnitTestIocContainer extends IocContainer { - constructor() { - super(); - } - public async getPythonMajorVersion(resource: Uri): Promise { - const procServiceFactory = this.serviceContainer.get(IProcessServiceFactory); - const procService = await procServiceFactory.create(resource); - const pythonVersion = await getPythonSemVer(procService); - if (pythonVersion) { - return pythonVersion.major; - } else { - return -1; // log warning already issued by underlying functions... - } - } - - public registerTestVisitors() { - this.serviceManager.add(ITestVisitor, TestFlatteningVisitor, 'TestFlatteningVisitor'); - this.serviceManager.add(ITestVisitor, TestFolderGenerationVisitor, 'TestFolderGenerationVisitor'); - this.serviceManager.add(ITestVisitor, TestResultResetVisitor, 'TestResultResetVisitor'); - } - - public registerTestStorage() { - this.serviceManager.addSingleton(ITestCollectionStorageService, TestCollectionStorageService); - } - - public registerTestsHelper() { - this.serviceManager.addSingleton(ITestsHelper, TestsHelper); - } - - public registerTestResultsHelper() { - this.serviceManager.add(ITestResultsService, TestResultsService); - } - - public registerTestParsers() { - this.serviceManager.add(ITestsParser, UnitTestTestsParser, UNITTEST_PROVIDER); - this.serviceManager.add(ITestsParser, PytestTestsParser, PYTEST_PROVIDER); - this.serviceManager.add(ITestsParser, NoseTestTestsParser, NOSETEST_PROVIDER); - } - - public registerTestDiscoveryServices() { - this.serviceManager.add(ITestDiscoveryService, UnitTestTestDiscoveryService, UNITTEST_PROVIDER); - this.serviceManager.add(ITestDiscoveryService, PytestTestDiscoveryService, PYTEST_PROVIDER); - this.serviceManager.add(ITestDiscoveryService, NoseTestDiscoveryService, NOSETEST_PROVIDER); - } - - public registerTestDiagnosticServices() { - this.serviceManager.addSingleton(IUnitTestDiagnosticService, UnitTestDiagnosticService); - } - - public registerTestManagers() { - this.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}'`); - } - } - }; - }); - } - - public registerTestManagerService() { - this.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); - }; - }); - } - - public registerMockUnitTestSocketServer() { - this.serviceManager.addSingleton(IUnitTestSocketServer, MockUnitTestSocketServer); - } - - public registerDataScienceTypes() { - this.serviceManager.addSingleton(IJupyterExecution, JupyterExecution); - this.serviceManager.addSingleton(IHistoryProvider, HistoryProvider); - this.serviceManager.add(IHistory, History); - this.serviceManager.add(INotebookImporter, JupyterImporter); - this.serviceManager.add(INotebookServer, JupyterServer); - this.serviceManager.addSingleton(ICodeCssGenerator, CodeCssGenerator); - } -} diff --git a/src/test/unittests/stoppingDiscoverAndTest.test.ts b/src/test/unittests/stoppingDiscoverAndTest.test.ts deleted file mode 100644 index 38c580eed10e..000000000000 --- a/src/test/unittests/stoppingDiscoverAndTest.test.ts +++ /dev/null @@ -1,96 +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/unittests/common/constants'; -import { ITestDiscoveryService } from '../../client/unittests/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(); - } - - 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, 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(); - - // 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, 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/unittests/unittest/unittest.argsService.unit.test.ts b/src/test/unittests/unittest/unittest.argsService.unit.test.ts deleted file mode 100644 index d17d29133a73..000000000000 --- a/src/test/unittests/unittest/unittest.argsService.unit.test.ts +++ /dev/null @@ -1,63 +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 { ILogger } from '../../../client/common/types'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { ArgumentsHelper } from '../../../client/unittests/common/argumentsHelper'; -import { IArgumentsHelper } from '../../../client/unittests/types'; -import { ArgumentsService as UnittestArgumentsService } from '../../../client/unittests/unittest/services/argsService'; - -suite('ArgsService: unittest', () => { - let argumentsService: UnittestArgumentsService; - - suiteSetup(() => { - const serviceContainer = typeMoq.Mock.ofType(); - const logger = typeMoq.Mock.ofType(); - - serviceContainer - .setup(s => s.get(typeMoq.It.isValue(ILogger), typeMoq.It.isAny())) - .returns(() => logger.object); - - const argsHelper = new ArgumentsHelper(serviceContainer.object); - - 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/unittests/unittest/unittest.diagnosticService.unit.test.ts b/src/test/unittests/unittest/unittest.diagnosticService.unit.test.ts deleted file mode 100644 index 5e9ce53ac5c8..000000000000 --- a/src/test/unittests/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/unittests/common/services/unitTestDiagnosticService'; -import { TestStatus } from '../../../client/unittests/common/types'; -import { PythonUnitTestMessageSeverity } from '../../../client/unittests/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(PythonUnitTestMessageSeverity.Error); - expectedPrefix = localize.UnitTests.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(PythonUnitTestMessageSeverity.Failure); - expectedPrefix = localize.UnitTests.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(PythonUnitTestMessageSeverity.Skip); - expectedPrefix = localize.UnitTests.testSkippedDiagnosticMessage(); - expectedSeverity = DiagnosticSeverity.Information; - }); - test('Message Prefix', () => { - assert.equal(actualPrefix, expectedPrefix); - }); - test('Severity', () => { - assert.equal(actualSeverity, expectedSeverity); - }); - }); -}); diff --git a/src/test/unittests/unittest/unittest.discovery.test.ts b/src/test/unittests/unittest/unittest.discovery.test.ts deleted file mode 100644 index ceb8c4f9fa03..000000000000 --- a/src/test/unittests/unittest/unittest.discovery.test.ts +++ /dev/null @@ -1,148 +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 { ConfigurationTarget } from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { IProcessServiceFactory } from '../../../client/common/process/types'; -import { CommandSource } from '../../../client/unittests/common/constants'; -import { ITestManagerFactory } from '../../../client/unittests/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('unitTest.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('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerUnitTestTypes(); - - // Mocks. - ioc.registerMockProcessTypes(); - } - - async function injectTestDiscoveryOutput(output: string) { - const procService = await ioc.serviceContainer.get(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('unitTest.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); - 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.Test_test1.test_A'), true, 'Test File not found'); - }); - - test('Discover Tests', async () => { - await updateSetting('unitTest.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); - 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.Test_test1.test_A'), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_two.py' && t.nameToRun === 'test_unittest_two.Test_test2.test_A2'), true, 'Test File not found'); - }); - - test('Discover Tests (pattern = *_test_*.py)', async () => { - await updateSetting('unitTest.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); - 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.Test_test3.test_A'), true, 'Test File not found'); - }); - - test('Setting cwd should return tests', async () => { - await updateSetting('unitTest.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); - 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/unittests/unittest/unittest.discovery.unit.test.ts b/src/test/unittests/unittest/unittest.discovery.unit.test.ts deleted file mode 100644 index fe54b33f7b1b..000000000000 --- a/src/test/unittests/unittest/unittest.discovery.unit.test.ts +++ /dev/null @@ -1,550 +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/unittests/common/constants'; -import { TestsHelper } from '../../../client/unittests/common/testUtils'; -import { TestFlatteningVisitor } from '../../../client/unittests/common/testVisitors/flatteningVisitor'; -import { ITestDiscoveryService, ITestRunner, ITestsParser, - Options, TestDiscoveryOptions, Tests, UnitTestParserOptions } from '../../../client/unittests/common/types'; -import { IArgumentsHelper } from '../../../client/unittests/types'; -import { TestDiscoveryService } from '../../../client/unittests/unittest/services/discoveryService'; -import { TestsParser } from '../../../client/unittests/unittest/services/parserService'; - -use(chaipromise); - -suite('Unit Tests - Unittest - Discovery', () => { - let discoveryService: ITestDiscoveryService; - let argsHelper: typeMoq.IMock; - let testParser: typeMoq.IMock; - let runner: typeMoq.IMock; - let serviceContainer: typeMoq.IMock; - const dir = path.join('a', 'b', 'c'); - const pattern = 'Pattern_To_Search_For'; - setup(() => { - serviceContainer = typeMoq.Mock.ofType(); - argsHelper = typeMoq.Mock.ofType(); - testParser = typeMoq.Mock.ofType(); - runner = typeMoq.Mock.ofType(); - - 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.once()); - 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(); - const token = typeMoq.Mock.ofType(); - 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); - 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.once()); - argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--start-directory'))) - .returns(() => dir) - .verifiable(typeMoq.Times.once()); - 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(); - const token = typeMoq.Mock.ofType(); - 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); - 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.once()); - argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--start-directory'))) - .returns(() => undefined) - .verifiable(typeMoq.Times.once()); - 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(); - const token = typeMoq.Mock.ofType(); - 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); - 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.once()); - 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(); - const token = typeMoq.Mock.ofType(); - 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); - 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.once()); - argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--pattern'))) - .returns(() => pattern) - .verifiable(typeMoq.Times.once()); - 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(); - const token = typeMoq.Mock.ofType(); - 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); - 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.once()); - argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--pattern'))) - .returns(() => undefined) - .verifiable(typeMoq.Times.once()); - 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(); - const token = typeMoq.Mock.ofType(); - 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); - 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.once()); - argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('--pattern'))) - .returns(() => undefined) - .verifiable(typeMoq.Times.once()); - 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(); - const token = typeMoq.Mock.ofType(); - 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'); - 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(); - const token = typeMoq.Mock.ofType(); - const wspace = typeMoq.Mock.ofType(); - 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(1); - - // 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(); - const token = typeMoq.Mock.ofType(); - const wspace = typeMoq.Mock.ofType(); - 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(1); - - // 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(); - const token = typeMoq.Mock.ofType(); - const wspace = typeMoq.Mock.ofType(); - 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(1); - - // 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(); - const token = typeMoq.Mock.ofType(); - const wspace = typeMoq.Mock.ofType(); - 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(1); - - // 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(); - const token = typeMoq.Mock.ofType(); - const wspace = typeMoq.Mock.ofType(); - 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(); - const token = typeMoq.Mock.ofType(); - const wspace = typeMoq.Mock.ofType(); - 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(); - const token = typeMoq.Mock.ofType(); - const wspace = typeMoq.Mock.ofType(); - 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/unittests/unittest/unittest.run.test.ts b/src/test/unittests/unittest/unittest.run.test.ts deleted file mode 100644 index 4acd798d3985..000000000000 --- a/src/test/unittests/unittest/unittest.run.test.ts +++ /dev/null @@ -1,308 +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 { ConfigurationTarget } from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { IProcessServiceFactory } from '../../../client/common/process/types'; -import { ArgumentsHelper } from '../../../client/unittests/common/argumentsHelper'; -import { CommandSource, UNITTEST_PROVIDER } from '../../../client/unittests/common/constants'; -import { TestRunner } from '../../../client/unittests/common/runner'; -import { ITestManagerFactory, ITestRunner, IUnitTestSocketServer, TestsToRun } from '../../../client/unittests/common/types'; -import { IArgumentsHelper, IArgumentsService, ITestManagerRunner, IUnitTestHelper } from '../../../client/unittests/types'; -import { UnitTestHelper } from '../../../client/unittests/unittest/helper'; -import { TestManagerRunner } from '../../../client/unittests/unittest/runner'; -import { ArgumentsService } from '../../../client/unittests/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'; - -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' -]; - -// tslint:disable-next-line:max-func-body-length -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('unitTest.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('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); - }); - - 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.serviceManager.add(IArgumentsService, ArgumentsService, UNITTEST_PROVIDER); - ioc.serviceManager.add(IArgumentsHelper, ArgumentsHelper); - ioc.serviceManager.add(ITestManagerRunner, TestManagerRunner, UNITTEST_PROVIDER); - ioc.serviceManager.add(ITestRunner, TestRunner); - ioc.serviceManager.add(IUnitTestHelper, UnitTestHelper); - } - - async function ignoreTestLauncher() { - const procService = await ioc.serviceContainer.get(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).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(IUnitTestSocketServer); - socketServer.reset(); - socketServer.addResults(results); - } - - test('Run Tests', async () => { - await updateSetting('unitTest.unittestArgs', ['-v', '-s', './tests', '-p', '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); - 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', async () => { - await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=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); - 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', async () => { - await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=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); - 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', async () => { - await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=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); - 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', async () => { - await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=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); - 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/unittests/unittest/unittest.test.ts b/src/test/unittests/unittest/unittest.test.ts deleted file mode 100644 index d4bf60eaac43..000000000000 --- a/src/test/unittests/unittest/unittest.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -'use strict'; - -import * as assert from 'assert'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { ConfigurationTarget } from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { CommandSource } from '../../../client/unittests/common/constants'; -import { - ITestManagerFactory, TestFile, - TestFunction, Tests, TestsToRun -} from '../../../client/unittests/common/types'; -import { - isPythonVersion, - 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('unitTest.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('unitTest.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri!, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerUnitTestTypes(); - ioc.registerProcessTypes(); - } - - test('Discover Tests (single test file)', async () => { - await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); - const factory = ioc.serviceContainer.get(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.Test_test1.test_A'), true, 'Test File not found'); - }); - - test('Discover Tests (many test files, subdir included)', async () => { - await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); - const factory = ioc.serviceContainer.get(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.Test_test1.test_A'), true, 'Test File one not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_two.py' && t.nameToRun === 'test_two.Test_test2.test_2A'), true, 'Test File two not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_three.py' && t.nameToRun === 'more_tests.test_three.Test_test3.test_3A'), true, 'Test File three not found'); - }); - - test('Run single test', async () => { - await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); - const factory = ioc.serviceContainer.get(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('_3A') - ); - 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 () { - // This test has not been working for many months in Python 3.4. Tracked by #2548. - if (await isPythonVersion('3.4')) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - - await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); - const factory = ioc.serviceContainer.get(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 () { - // This test has not been working for many months in Python 3.4. Tracked by #2548. - if (await isPythonVersion('3.4')) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - - await updateSetting('unitTest.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); - const factory = ioc.serviceContainer.get(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/utils/fs.ts b/src/test/utils/fs.ts new file mode 100644 index 000000000000..13f46bd38f82 --- /dev/null +++ b/src/test/utils/fs.ts @@ -0,0 +1,323 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'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, +): Promise<{ filePath: string; cleanupCallback: Function }> { + const options: any = { postfix: extension }; + if (temporaryDirectory) { + options.dir = temporaryDirectory; + } + + return new Promise<{ filePath: string; cleanupCallback: Function }>((resolve, reject) => { + tmp.file(options, (err, tmpFile, _fd, cleanupCallback) => { + if (err) { + return reject(err); + } + resolve({ filePath: tmpFile, cleanupCallback: cleanupCallback }); + }); + }); +} + +// 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', 'exe']` + * `` `['x/y/z/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 + * `` # directories cannot be marked as executable + * `/` # directories cannot be marked as executable + * `` # 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 + * + * + * file5 + * dir2/ + * file6 + * + * ./x/y/z/root2/ + * dir3/ + * subdir3_1/ + * file8 + * ./a/b/root3/ + * + * `.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 { + 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 { + // "touch" the file. + await fsapi.ensureFile(filename as string); + await fsapi.chmod(filename as string, 0o755); +} + +async function ensureSymlink(target: string, filename: string): Promise { + 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 new file mode 100644 index 000000000000..ece3b7731c5c --- /dev/null +++ b/src/test/utils/interpreters.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { Architecture } from '../../client/common/utils/platform'; +import { EnvironmentType, PythonEnvironment } from '../../client/pythonEnvironments/info'; + +/** + * Creates a PythonInterpreter object for testing purposes, with unique name, version and path. + * If required a custom name, version and the like can be provided. + */ +export function createPythonInterpreter(info?: Partial): PythonEnvironment { + const rnd = new Date().getTime().toString(); + return { + displayName: `Something${rnd}`, + architecture: Architecture.Unknown, + path: `somePath${rnd}`, + sysPrefix: `someSysPrefix${rnd}`, + sysVersion: `1.1.1`, + envType: EnvironmentType.Unknown, + ...(info || {}), + }; +} diff --git a/src/test/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 de32f703b227..b7ea2bc549a0 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -3,64 +3,105 @@ '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 = {}; -const mockedVSCodeNamespaces: { [P in keyof VSCode]?: TypeMoq.IMock } = {}; +export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: VSCode[P] } = {}; const originalLoad = Module._load; function generateMock(name: K): void { - const mockedObj = TypeMoq.Mock.ofType(); - mockedVSCode[name] = mockedObj.object; + const mockedObj = mock(); + (mockedVSCode as any)[name] = instance(mockedObj); mockedVSCodeNamespaces[name] = mockedObj as any; } +class MockClipboard { + private text: string = ''; + public readText(): Promise { + return Promise.resolve(this.text); + } + public async writeText(value: string): Promise { + this.text = value; + } +} export function initialize() { generateMock('workspace'); generateMock('window'); generateMock('commands'); generateMock('languages'); + generateMock('extensions'); generateMock('env'); generateMock('debug'); generateMock('scm'); + generateMock('notebooks'); + + // Use mock clipboard fo testing purposes. + const clipboard = new MockClipboard(); + 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>(); + const packageJson = mock(); + const contributes = mock(); + 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, parent) { + Module._load = function (request: any, _parent: any) { if (request === 'vscode') { return mockedVSCode; } - if (request === 'vscode-extension-telemetry') { - return { default: vscMockTelemetryReporter }; + if (request === '@vscode/extension-telemetry') { + return { default: vscMockTelemetryReporter as any }; + } + // less files need to be in import statements to be converted to css + // But we don't want to try to load them in the mock vscode + if (/\.less$/.test(request)) { + return; } return originalLoad.apply(this, arguments); }; } -mockedVSCode.Disposable = vscodeMocks.vscMock.Disposable as any; -mockedVSCode.EventEmitter = vscodeMocks.vscMock.EventEmitter; -mockedVSCode.CancellationTokenSource = vscodeMocks.vscMock.CancellationTokenSource; -mockedVSCode.CompletionItemKind = vscodeMocks.vscMock.CompletionItemKind; -mockedVSCode.SymbolKind = vscodeMocks.vscMock.SymbolKind; -mockedVSCode.Uri = vscodeMocks.vscMock.Uri as any; +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.EventEmitter = vscodeMocks.vscMock.EventEmitter; mockedVSCode.ConfigurationTarget = vscodeMocks.vscMockExtHostedTypes.ConfigurationTarget; mockedVSCode.StatusBarAlignment = vscodeMocks.vscMockExtHostedTypes.StatusBarAlignment; mockedVSCode.SignatureHelp = vscodeMocks.vscMockExtHostedTypes.SignatureHelp; @@ -70,15 +111,67 @@ mockedVSCode.WorkspaceEdit = vscodeMocks.vscMockExtHostedTypes.WorkspaceEdit; mockedVSCode.RelativePattern = vscodeMocks.vscMockExtHostedTypes.RelativePattern; mockedVSCode.ProgressLocation = vscodeMocks.vscMockExtHostedTypes.ProgressLocation; mockedVSCode.ViewColumn = vscodeMocks.vscMockExtHostedTypes.ViewColumn; +mockedVSCode.TextEditorRevealType = vscodeMocks.vscMockExtHostedTypes.TextEditorRevealType; +mockedVSCode.TreeItem = vscodeMocks.vscMockExtHostedTypes.TreeItem; +mockedVSCode.TreeItemCollapsibleState = vscodeMocks.vscMockExtHostedTypes.TreeItemCollapsibleState; +(mockedVSCode 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.FileType; +mockedVSCode.UIKind = vscodeMocks.UIKind; +mockedVSCode.FileSystemError = vscodeMocks.vscMockExtHostedTypes.FileSystemError; +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) {} +}; + +// 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; +} -// This API is used in src/client/telemetry/telemetry.ts -const extensions = TypeMoq.Mock.ofType(); -extensions.setup(e => e.all).returns(() => []); -const extension = TypeMoq.Mock.ofType>(); -const packageJson = TypeMoq.Mock.ofType(); -const contributes = TypeMoq.Mock.ofType(); -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; +// 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 527b852ab6ad..000000000000 --- a/src/test/workspaceSymbols/common.ts +++ /dev/null @@ -1,8 +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 5b425f8fc357..000000000000 --- a/src/test/workspaceSymbols/generator.unit.test.ts +++ /dev/null @@ -1,111 +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; - let generator: Generator; - let factory: typemoq.IMock; - let shell: IApplicationShell; - let processService: IProcessService; - let fs: IFileSystem; - const folderUri = Uri.parse(path.join('a', 'b', 'c')); - setup(() => { - pythonSettings = typemoq.Mock.ofType(); - configurationService = mock(ConfigurationService); - factory = typemoq.Mock.ofType(); - 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(); - 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) => 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) => 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/provider.unit.test.ts b/src/test/workspaceSymbols/provider.unit.test.ts deleted file mode 100644 index 63bc69515e8e..000000000000 --- a/src/test/workspaceSymbols/provider.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 - -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 65859ed0254a..51d218783041 100644 --- a/src/testMultiRootWkspc/multi.code-workspace +++ b/src/testMultiRootWkspc/multi.code-workspace @@ -35,14 +35,8 @@ "python.linting.pydocstyleEnabled": false, "python.linting.pylamaEnabled": false, "python.linting.pylintEnabled": true, - "python.linting.pep8Enabled": false, + "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 c404e94945a9..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 -} \ No newline at end of file +{} 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/smokeTests/testExecInTerminal.py b/src/testMultiRootWkspc/smokeTests/testExecInTerminal.py index 84c0c03d6ba0..d83d46a740d9 100644 --- a/src/testMultiRootWkspc/smokeTests/testExecInTerminal.py +++ b/src/testMultiRootWkspc/smokeTests/testExecInTerminal.py @@ -1,7 +1,16 @@ +import getopt import sys import os +optlist, args = getopt.getopt(sys.argv, '') +# If the caller has not specified the output file, create one for them with +# the same name as the caller script, but with a .log extension. log_file = os.path.splitext(sys.argv[0])[0] + '.log' + +# If the output file is given, use that instead. +if len(args) == 2: + log_file = args[1] + with open(log_file, "a") as f: f.write(sys.executable) diff --git a/src/testMultiRootWkspc/workspace1/.vscode/.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 import ` 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/settings.json b/src/testMultiRootWkspc/workspace1/.vscode/settings.json index f4d89e3bc0e4..1e5ea7556081 100644 --- a/src/testMultiRootWkspc/workspace1/.vscode/settings.json +++ b/src/testMultiRootWkspc/workspace1/.vscode/settings.json @@ -1,5 +1,3 @@ { - "python.linting.enabled": false, - "python.linting.flake8Enabled": true, - "python.linting.pylintEnabled": false + "python.linting.enabled": true } 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/workspace4/.env6 b/src/testMultiRootWkspc/workspace4/.env6 new file mode 100644 index 000000000000..76459c0f68cc --- /dev/null +++ b/src/testMultiRootWkspc/workspace4/.env6 @@ -0,0 +1,3 @@ +REPO=/home/user/git/foobar +PYTHONPATH=${REPO}/foo:${REPO}/bar +PYTHON=${BINDIR}/python3 diff --git a/src/testMultiRootWkspc/workspace5/.vscode/settings.json b/src/testMultiRootWkspc/workspace5/.vscode/settings.json index 0db3279e44b0..0967ef424bce 100644 --- a/src/testMultiRootWkspc/workspace5/.vscode/settings.json +++ b/src/testMultiRootWkspc/workspace5/.vscode/settings.json @@ -1,3 +1 @@ -{ - -} +{} 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/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd-nowait.py b/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-nowait.py similarity index 100% rename from src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd-nowait.py rename to src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-nowait.py diff --git a/src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.py b/src/testMultiRootWkspc/workspace5/remoteDebugger-start.py similarity index 100% rename from src/testMultiRootWkspc/workspace5/remoteDebugger-start-with-ptvsd.py rename to src/testMultiRootWkspc/workspace5/remoteDebugger-start.py 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/syntaxes/pip-requirements.tmLanguage.json b/syntaxes/pip-requirements.tmLanguage.json index 869efbe7834a..ea0c69b19f65 100644 --- a/syntaxes/pip-requirements.tmLanguage.json +++ b/syntaxes/pip-requirements.tmLanguage.json @@ -59,7 +59,7 @@ { "explanation": "environment markers", "match": ";\\s*(python_version|python_full_version|os_name|sys_platform|platform_release|platform_system|platform_version|platform_machine|platform_python_implementation|implementation_name|implementation_version|extra)\\s*(<|<=|!=|==|>=|>|~=|===)", - "captures":{ + "captures": { "1": { "name": "entity.name.selector" }, diff --git a/tpn/README.md b/tpn/README.md deleted file mode 100644 index 1b70830ef137..000000000000 --- a/tpn/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Third-party notices file generation - -Assuming you have created a virtual environment (for Python 3.7), -installed the `requirements.txt` dependencies, and activated the virtual environment: - -```shell -$ python ./tpn --npm package-lock.json --config tpn/distribution.toml ThirdPartyNotices-Distribution.txt -``` diff --git a/tpn/__main__.py b/tpn/__main__.py deleted file mode 100644 index 343e22ced2d1..000000000000 --- a/tpn/__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("tpn", run_name="__main__", alter_sys=True) diff --git a/tpn/distribution.toml b/tpn/distribution.toml deleted file mode 100644 index 4aaf75d65077..000000000000 --- a/tpn/distribution.toml +++ /dev/null @@ -1,1510 +0,0 @@ -[metadata] -header = """ -THIRD-PARTY SOFTWARE NOTICES AND INFORMATION -Do Not Translate or Localize - -Microsoft Python extension for Visual Studio Code incorporates third party material from the projects listed below. -""" - -[[project]] -name = "@jupyterlab/coreutils" -version = "2.1.4" -url = "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-2.1.4.tgz" -purpose = "npm" -license = """ -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. -""" - -[[project]] -name = "@jupyterlab/observables" -version = "2.0.7" -url = "https://registry.npmjs.org/@jupyterlab/observables/-/observables-2.0.7.tgz" -purpose = "npm" -license = """ -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. -""" - -[[project]] -name = "@jupyterlab/services" -version = "3.1.4" -url = "https://registry.npmjs.org/@jupyterlab/services/-/services-3.1.4.tgz" -purpose = "npm" -license = """ -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. -""" - -[[project]] -name = "@phosphor/algorithm" -version = "1.1.2" -url = "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.1.2.tgz" -purpose = "npm" -license = """ -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. -""" - -[[project]] -name = "@phosphor/collections" -version = "1.1.2" -url = "https://registry.npmjs.org/@phosphor/collections/-/collections-1.1.2.tgz" -purpose = "npm" -license = """ -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. - -""" - -[[project]] -name = "@phosphor/coreutils" -version = "1.3.0" -url = "https://registry.npmjs.org/@phosphor/coreutils/-/coreutils-1.3.0.tgz" -purpose = "npm" -license = """ -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. - -""" - -[[project]] -name = "@phosphor/disposable" -version = "1.1.2" -url = "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.1.2.tgz" -purpose = "npm" -license = """ -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. -""" - -[[project]] -name = "@phosphor/messaging" -version = "1.2.2" -url = "https://registry.npmjs.org/@phosphor/messaging/-/messaging-1.2.2.tgz" -purpose = "npm" -license = """ -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. - -""" - -[[project]] -name = "@phosphor/signaling" -version = "1.2.2" -url = "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.2.2.tgz" -purpose = "npm" -license = """ -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. - -""" - -[[project]] -name = "_pydev_calltip_util.py" -version = "(for PyDev.Debugger)" -url = "https://github.com/fabioz/PyDev.Debugger/blob/master/_pydev_bundle/_pydev_calltip_util.py" -purpose = "explicit" -license = """ -Copyright (c) Yuli Fitterman - -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. -""" - -[[project]] -name = "angular.io" -version = "(for RxJS 5.5)" -url = "https://angular.io/" -purpose = "explicit" -license = """ -The MIT License - -Copyright (c) 2014-2017 Google, 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. -""" - -[[project]] -name = "assert-plus" -version = "1.0.0" -url = "https://github.com/joyent/node-assert-plus/tree/v1.0.0" -purpose = "npm" -license = """ -The MIT License (MIT) -Copyright (c) 2012 Mark Cavage - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -[[project]] -name = "babel-polyfill" -version = "6.26.0" -url = "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz" -purpose = "npm" -license = """ -MIT License - -Copyright (c) 2014-2018 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. - -""" - -[[project]] -name = "babel-runtime" -version = "6.26.0" -url = "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz" -purpose = "npm" -license = """ -MIT License - -Copyright (c) 2014-2018 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. -""" - -[[project]] -name = "bcrypt-pbkdf" -version = "1.0.1" -url = "https://www.npmjs.com/package/bcrypt-pbkdf" -purpose = "npm" -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. - -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. -""" - -[[project]] -name = "IPython" -version = "(for PyDev.Debugger)" -url = "https://ipython.org/" -purpose = "explicit" -license = """ -Copyright (c) 2008-2010, IPython Development Team -Copyright (c) 2001-2007, Fernando Perez. -Copyright (c) 2001, Janko Hauser -Copyright (c) 2001, Nathaniel Gray - -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 IPython 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. -""" - -[[project]] -name = "isarray" -version = "1.0.0" -url = "https://github.com/juliangruber/isarray/blob/v1.0.0" -purpose = "npm" -license = """ -(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. -""" - -[[project]] -name = "isort" -version = "4.3.4" -url = "https://github.com/timothycrosley/isort/tree/4.3.4" -purpose = "PyPI" -license = """ -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. -""" - -[[project]] -name = "Jedi" -version = "0.12.0" -url = "https://github.com/davidhalter/jedi/tree/v0.12.0" -purpose = "PyPI" -license = """ -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. -""" - -[[project]] -name = "json-schema" -version = "0.2.3" -url = "https://www.npmjs.com/package/json-schema" # References the Dojo Foundation's license: https://github.com/dojo/meta -purpose = "npm" -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. - -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. -""" - -[[project]] -name = "jsonify" -version = "0.0.0" -url = "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" -purpose = "npm" -license = """ -public domain -""" - -[[project]] -name = "martinez-polygon-clipping" -version = "0.1.5" -url = "https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.1.5.tgz" -purpose = "npm" -license = """ -MIT License - -Copyright (c) 2018 Alexander Milevski - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -""" - -[[project]] -name = "node-stream-zip" -version = "1.6.0" -url = "https://github.com/antelle/node-stream-zip/tree/1.6.0" -purpose = "npm" -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. -""" - -[[project]] -name = "options" -version = "0.0.6" -url = "https://registry.npmjs.org/options/-/options-0.0.6.tgz" -purpose = "npm" -license = """ -(The MIT License) - -Copyright (c) 2012 Einar Otto Stangvik <einaros@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. -""" - -[[project]] -name = "parso" -version = "0.2.1" -url = "https://github.com/davidhalter/parso/tree/v0.2.1" -purpose = "PyPI" -license = """ -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. -""" - -[[project]] -name = "psl" -version = "1.1.29" -url = "https://github.com/wrangr/psl/tree/v1.1.29" -purpose = "npm" -license = """ -The MIT License (MIT) - -Copyright (c) 2017 Lupo Montero - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - -[[project]] -name = "ptvsd" -version = "4.2.0" -url = "https://github.com/Microsoft/ptvsd/tree/v4.2.0" -purpose = "PyPI" -license = """ - ptvsd - - 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. -""" - -[[project]] -name = "py2app" -version = "(for PyDev.Debugger)" -url = "https://bitbucket.org/ronaldoussoren/py2app" -purpose = "explicit" -license = """ -This is the MIT license. This software may also be distributed under the same terms as Python (the PSF license). - -Copyright (c) 2004 Bob Ippolito. - -Some parts copyright (c) 2010-2014 Ronald Oussoren - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" - -[[project]] -name = "PyDev.Debugger" -version = "(for ptvsd 4)" -url = "https://pypi.org/project/pydevd/" -purpose = "explicit" -license = """ -Eclipse Public License - v 1.0 - -THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC -LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM -CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - -a) in the case of the initial Contributor, the initial code and documentation - distributed under this Agreement, and -b) in the case of each subsequent Contributor: - i) changes to the Program, and - ii) additions to the Program; - - where such changes and/or additions to the Program originate from and are - distributed by that particular Contributor. A Contribution 'originates' - from a Contributor if it was added to the Program by such Contributor - itself or anyone acting on such Contributor's behalf. Contributions do not - include additions to the Program which: (i) are separate modules of - software distributed in conjunction with the Program under their own - license agreement, and (ii) are not derivative works of the Program. - -"Contributor" means any person or entity that distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which are -necessarily infringed by the use or sale of its Contribution alone or when -combined with the Program. - -"Program" means the Contributions distributed in accordance with this -Agreement. - -"Recipient" means anyone who receives the Program under this Agreement, -including all Contributors. - -2. GRANT OF RIGHTS - a) Subject to the terms of this Agreement, each Contributor hereby grants - Recipient a non-exclusive, worldwide, royalty-free copyright license to - reproduce, prepare derivative works of, publicly display, publicly - perform, distribute and sublicense the Contribution of such Contributor, - if any, and such derivative works, in source code and object code form. - b) Subject to the terms of this Agreement, each Contributor hereby grants - Recipient a non-exclusive, worldwide, royalty-free patent license under - Licensed Patents to make, use, sell, offer to sell, import and otherwise - transfer the Contribution of such Contributor, if any, in source code and - object code form. This patent license shall apply to the combination of - the Contribution and the Program if, at the time the Contribution is - added by the Contributor, such addition of the Contribution causes such - combination to be covered by the Licensed Patents. The patent license - shall not apply to any other combinations which include the Contribution. - No hardware per se is licensed hereunder. - c) Recipient understands that although each Contributor grants the licenses - to its Contributions set forth herein, no assurances are provided by any - Contributor that the Program does not infringe the patent or other - intellectual property rights of any other entity. Each Contributor - disclaims any liability to Recipient for claims brought by any other - entity based on infringement of intellectual property rights or - otherwise. As a condition to exercising the rights and licenses granted - hereunder, each Recipient hereby assumes sole responsibility to secure - any other intellectual property rights needed, if any. For example, if a - third party patent license is required to allow Recipient to distribute - the Program, it is Recipient's responsibility to acquire that license - before distributing the Program. - d) Each Contributor represents that to its knowledge it has sufficient - copyright rights in its Contribution, if any, to grant the copyright - license set forth in this Agreement. - -3. REQUIREMENTS - -A Contributor may choose to distribute the Program in object code form under -its own license agreement, provided that: - - a) it complies with the terms and conditions of this Agreement; and - b) its license agreement: - i) effectively disclaims on behalf of all Contributors all warranties - and conditions, express and implied, including warranties or - conditions of title and non-infringement, and implied warranties or - conditions of merchantability and fitness for a particular purpose; - ii) effectively excludes on behalf of all Contributors all liability for - damages, including direct, indirect, special, incidental and - consequential damages, such as lost profits; - iii) states that any provisions which differ from this Agreement are - offered by that Contributor alone and not by any other party; and - iv) states that source code for the Program is available from such - Contributor, and informs licensees how to obtain it in a reasonable - manner on or through a medium customarily used for software exchange. - -When the Program is made available in source code form: - - a) it must be made available under this Agreement; and - b) a copy of this Agreement must be included with each copy of the Program. - Contributors may not remove or alter any copyright notices contained - within the Program. - -Each Contributor must identify itself as the originator of its Contribution, -if -any, in a manner that reasonably allows subsequent Recipients to identify the -originator of the Contribution. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities with -respect to end users, business partners and the like. While this license is -intended to facilitate the commercial use of the Program, the Contributor who -includes the Program in a commercial product offering should do so in a manner -which does not create potential liability for other Contributors. Therefore, -if a Contributor includes the Program in a commercial product offering, such -Contributor ("Commercial Contributor") hereby agrees to defend and indemnify -every other Contributor ("Indemnified Contributor") against any losses, -damages and costs (collectively "Losses") arising from claims, lawsuits and -other legal actions brought by a third party against the Indemnified -Contributor to the extent caused by the acts or omissions of such Commercial -Contributor in connection with its distribution of the Program in a commercial -product offering. The obligations in this section do not apply to any claims -or Losses relating to any actual or alleged intellectual property -infringement. In order to qualify, an Indemnified Contributor must: -a) promptly notify the Commercial Contributor in writing of such claim, and -b) allow the Commercial Contributor to control, and cooperate with the -Commercial Contributor in, the defense and any related settlement -negotiations. The Indemnified Contributor may participate in any such claim at -its own expense. - -For example, a Contributor might include the Program in a commercial product -offering, Product X. That Contributor is then a Commercial Contributor. If -that Commercial Contributor then makes performance claims, or offers -warranties related to Product X, those performance claims and warranties are -such Commercial Contributor's responsibility alone. Under this section, the -Commercial Contributor would have to defend claims against the other -Contributors related to those performance claims and warranties, and if a -court requires any other Contributor to pay any damages as a result, the -Commercial Contributor must pay those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED 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. Each -Recipient is solely responsible for determining the appropriateness of using -and distributing the Program and assumes all risks associated with its -exercise of rights under this Agreement , including but not limited to the -risks and costs of program errors, compliance with applicable laws, damage to -or loss of data, programs or equipment, and unavailability or interruption of -operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY -CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION -LOST PROFITS), 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 OR DISTRIBUTION OF THE PROGRAM OR THE -EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY -OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under -applicable law, it shall not affect the validity or enforceability of the -remainder of the terms of this Agreement, and without further action by the -parties hereto, such provision shall be reformed to the minimum extent -necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Program itself -(excluding combinations of the Program with other software or hardware) -infringes such Recipient's patent(s), then such Recipient's rights granted -under Section 2(b) shall terminate as of the date such litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it fails to -comply with any of the material terms or conditions of this Agreement and does -not cure such failure in a reasonable period of time after becoming aware of -such noncompliance. If all Recipient's rights under this Agreement terminate, -Recipient agrees to cease use and distribution of the Program as soon as -reasonably practicable. However, Recipient's obligations under this Agreement -and any licenses granted by Recipient relating to the Program shall continue -and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, but in -order to avoid inconsistency the Agreement is copyrighted and may only be -modified in the following manner. The Agreement Steward reserves the right to -publish new versions (including revisions) of this Agreement from time to -time. No one other than the Agreement Steward has the right to modify this -Agreement. The Eclipse Foundation is the initial Agreement Steward. The -Eclipse Foundation may assign the responsibility to serve as the Agreement -Steward to a suitable separate entity. Each new version of the Agreement will -be given a distinguishing version number. The Program (including -Contributions) may always be distributed subject to the version of the -Agreement under which it was received. In addition, after a new version of the -Agreement is published, Contributor may elect to distribute the Program -(including its Contributions) under the new version. Except as expressly -stated in Sections 2(a) and 2(b) above, Recipient receives no rights or -licenses to the intellectual property of any Contributor under this Agreement, -whether expressly, by implication, estoppel or otherwise. All rights in the -Program not expressly granted under this Agreement are reserved. - -This Agreement is governed by the laws of the State of New York and the -intellectual property laws of the United States of America. No party to this -Agreement will bring a legal action under this Agreement more than one year -after the cause of action arose. Each party waives its rights to a jury trial in -any resulting litigation. -""" - -[[project]] -name = "remark-parse" -version = "5.0.0" -url = "https://registry.npmjs.org/remark-parse/-/remark-parse-5.0.0.tgz" -purpose = "npm" -license = """ -(The MIT License) - -Copyright (c) 2014-2016 Titus Wormer -Copyright (c) 2011-2014, Christopher Jeffrey (https://github.com/chjj/) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -[[project]] -name = "setImmediate" -version = "(for RxJS 5.5)" -url = "https://github.com/YuzuJS/setImmediate" -purpose = "explicit" -license = """ -Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" - -[[project]] -name = "sizzle" -version = "(for lodash 4.17)" -url = "https://sizzlejs.com/" -purpose = "explicit" -license = """ -Copyright (c) 2009 John Resig - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - -[[project]] -name = "string-hash" -version = "1.1.3" -url = "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz" -purpose = "npm" -license = """ -To the extend possible by law, The Dark Sky Company, LLC has [waived all -copyright and related or neighboring rights][cc0] to this library. - -[cc0]: http://creativecommons.org/publicdomain/zero/1.0/ -""" - -[[project]] -name = "stylis-rule-sheet" -version = "0.0.10" -url = "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz" -purpose = "npm" -license = """ -MIT License - -Copyright (c) 2016 Sultan Tarimo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -""" - -[[project]] -name = "svg-path-bounding-box" -version = "1.0.4" -url = "https://registry.npmjs.org/svg-path-bounding-box/-/svg-path-bounding-box-1.0.4.tgz" -purpose = "npm" -license = """ -MIT License - -Copyright (c) 2016 Sultan Tarimo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -""" - -[[project]] -name = "throttleit" -version = "1.0.0" -url = "https://github.com/component/throttle/tree/1.0.0" -purpose = "npm" -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. -""" - -[[project]] -name = "tree-kill" -version = "1.2.0" -url = "https://github.com/pkrumins/node-tree-kill" -purpose = "npm" -license = """ -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. -""" - -[[project]] -name = "trim" -version = "0.0.1" -url = "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz" -purpose = "npm" -license = """ -(The MIT License) - -Copyright (c) 2012 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.XXX -""" - -[[project]] -name = "typescript-char" -version = "0.0.0" -url = "https://github.com/mason-lang/typescript-char" -purpose = "npm" -license = "http://unlicense.org/UNLICENSE" - -[[project]] -name = "webpack" -version = "(for lodash 4)" -url = "https://webpack.js.org/" -purpose = "explicit" -license = """ -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. -""" - -[[project]] -name = "uniqid" -version = "5.0.3" -url = "https://registry.npmjs.org/uniqid/-/uniqid-5.0.3.tgz" -purpose = "npm" -license = """ -(The MIT License) - -Copyright (c) 2014 Halász Ádám - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" - -[[project]] -name = "untangle" -version = "(for ptvsd 4)" -url = "https://pypi.org/project/untangle/" -purpose = "explicit" -license = """ -# Author: Christian Stefanescu - -# Contributions from: - -Florian Idelberger -Apalala - -// Copyright (c) 2011 - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. - """ - - [[project]] -name = "viz-annotation" -version = "0.0.1-3" -url = "https://registry.npmjs.org/viz-annotation/-/viz-annotation-0.0.1-3.tgz" -purpose = "npm" -license = """ -[Default ISC license] - -Copyright 2018 viz-annotation developers - -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. - -""" - - -[[project]] -name = "winreg" -version = "1.2.4" -url = "https://github.com/fresc81/node-winreg/tree/v1.2.4" -purpose = "npm" -license = """ -This project is released under [BSD 2-Clause License](http://opensource.org/licenses/BSD-2-Clause). - -Copyright (c) 2016, Paul Bottin 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. -""" - -[[project]] -name = "ws" -version = "1.1.5" -url = "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz" -purpose = "npm" -license = """ -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. - -""" diff --git a/tpn/requirements.txt b/tpn/requirements.txt deleted file mode 100644 index 2d03b19800a7..000000000000 --- a/tpn/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -aiohttp~=3.4.4 -docopt~=0.6.2 -pytest~=3.6.0 -pytest-asyncio~=0.8.0 -pytoml~=0.1.15 diff --git a/tpn/tpn/__main__.py b/tpn/tpn/__main__.py deleted file mode 100644 index cee22ba7d5e2..000000000000 --- a/tpn/tpn/__main__.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -"""Third-party notices generation. - -Usage: tpn [--npm=] [--npm-overrides=] --config= - -Options: - --npm= Path to a package-lock.json for npm. - --npm-overrides= Path to a JSON file containing an array of names to override "dev" in . - --config= Path to the configuration file. - -""" -import asyncio -import json -import pathlib -import sys -import textwrap - -import docopt -import pytoml as toml - -from . import config -from . import tpnfile -from . import npm - - -ACCEPTABLE_PURPOSES = frozenset({"explicit", "npm", "PyPI"}) - - -async def handle_index(module, raw_path, config_projects, cached_projects, overrides_path=None): - _, _, index_name = module.__name__.rpartition(".") - with open(raw_path, encoding="utf-8") as file: - raw_data = file.read() - if overrides_path: - with open(overrides_path, encoding="utf-8") as file: - raw_overrides_data = file.read() - else: - raw_overrides_data = None - requested_projects = await module.projects_from_data(raw_data, raw_overrides_data) - projects, stale = config.sort(index_name, config_projects, requested_projects) - for name, details in projects.items(): - print(f"{name} {details.version}: sourced from configuration file") - valid_cache_entries = tpnfile.sort(cached_projects, requested_projects) - for name, details in valid_cache_entries.items(): - print(f"{name} {details.version}: sourced from TPN cache") - projects.update(valid_cache_entries) - failures = await module.fill_in_licenses(requested_projects) - projects.update(requested_projects) - return projects, stale, failures - - -def main(tpn_path, *, config_path, npm_path=None, npm_overrides=None, pypi_path=None): - tpn_path = pathlib.Path(tpn_path) - config_path = pathlib.Path(config_path) - config_data = toml.loads(config_path.read_text(encoding="utf-8")) - config_projects = config.get_projects(config_data, ACCEPTABLE_PURPOSES) - projects = config.get_explicit_entries(config_projects) - if tpn_path.exists(): - cached_projects = tpnfile.parse_tpn(tpn_path.read_text(encoding="utf-8")) - else: - cached_projects = {} - tasks = [] - if npm_path: - tasks.append(handle_index(npm, npm_path, config_projects, cached_projects, npm_overrides)) - if pypi_path: - tasks.append(handle_index(pypi, pypi_path, config_projects, cached_projects)) - loop = asyncio.get_event_loop() - print() - gathered = loop.run_until_complete(asyncio.gather(*tasks)) - print() - stale = {} - failures = {} - for found_projects, found_stale, found_failures in gathered: - projects.update(found_projects) - stale.update(found_stale) - failures.update(found_failures) - for name in stale: - print("STALE in config file:", name) - if failures: - print("*" * 20) # Make failure stand out more. - for name, details in failures.items(): - print( - f"FAILED for {name} {details.version} @ {details.url}: {details.error}" - ) - print(textwrap.dedent(f""" - [[project]] - name = "{name}" - version = "{details.version}" - url = "{details.url}" - purpose = "{details.purpose or "XXX"}" - license = \"\"\" - XXX - \"\"\" - """)) - print(f"Could not find a license for {len(failures)} projects") - sys.exit(1) - with open(tpn_path, "w", encoding="utf-8", newline="\n") as file: - file.write(tpnfile.generate_tpn(config_data, projects)) - - -if __name__ == "__main__": - arguments = docopt.docopt(__doc__) - main( - arguments[""], - config_path=arguments["--config"], - npm_path=arguments["--npm"], - npm_overrides=arguments["--npm-overrides"], - ) diff --git a/tpn/tpn/config.py b/tpn/tpn/config.py deleted file mode 100644 index 099e876e47b5..000000000000 --- a/tpn/tpn/config.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import annotations - -import dataclasses -import enum - -from . import data - - -@dataclasses.dataclass -class ConfigProject(data.Project): - """Projects from a TOML configuration file.""" - - license: str - # Must be optional due to 'error' being optional in base class. - purpose: Optional[str] = None - - -SECTIONS = {"metadata", "project"} -FIELDS = {"name", "version", "url", "purpose", "license"} - - -def get_projects(config, acceptable_purposes): - """Pull out projects as specified in a configuration file.""" - found_sections = frozenset(config.keys()) - if found_sections != SECTIONS: - raise ValueError(f"Configuration file sections incorrect: {found_sections!r} != {SECTIONS!r}") - projects = {} - for project_data in config["project"]: - if not all(key in project_data for key in FIELDS): - name = project_data.get("name", "") - missing_keys = FIELDS.difference(project_data.keys()) - raise KeyError(f"{name!r} is missing the keys {sorted(missing_keys)}") - if project_data["purpose"] not in acceptable_purposes: - raise ValueError( - f"{project_data['name']!r} has a purpose of {project_data['purpose']!r}" - f" which is not one of {sorted(acceptable_purposes)}" - ) - projects[project_data["name"]] = ConfigProject(**project_data) - return projects - - -def get_explicit_entries(config_projects): - """Pull out and return the projects in the config that were explicitly entered. - - The projects in the returned dict are deleted from config_projects. - - """ - print("Including PyPI projects explicitly from configuration") - explicit_projects = { - name: details - for name, details in config_projects.items() - # TODO: Drop "PyPI" once appropriate PyPI support has been added. - if details.purpose in {"explicit", "PyPI"} - } - for project in explicit_projects: - del config_projects[project] - return explicit_projects - - -def sort(purpose, config_projects, requested_projects): - """Sort projects in the config for the specified 'purpose' into valid and stale entries. - - The config_projects mapping will have all 'purpose' projects deleted from it - in the end. The requested_projects mapping will have any project which was - appropriately found in config_projects deleted. In the end: - - - config_projects will have no projects related to 'purpose' left. - - requested_projects will have projects for which no match in config_projects - was found. - - The first returned item will be all projects which had a match in both - config_projects and requested_projects for 'purpose' - - The second item returned will be all projects which match 'purpose' that - were not placed into the first returned item - - """ - projects = {} - stale = {} - config_subset = { - project: details - for project, details in config_projects.items() - if details.purpose == purpose - } - for name, details in config_subset.items(): - del config_projects[name] - config_version = details.version - match = False - if name in requested_projects: - requested_version = requested_projects[name].version - if config_version == requested_version: - projects[name] = details - del requested_projects[name] - match = True - if not match: - stale[name] = details - - return projects, stale diff --git a/tpn/tpn/data.py b/tpn/tpn/data.py deleted file mode 100644 index 67ed9630242b..000000000000 --- a/tpn/tpn/data.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import annotations - -import dataclasses - - -@dataclasses.dataclass -class Project: - """Represents the details of a project.""" - - name: str - version: str - url: str - license: Optional[str] = None - error: Optional[Exception] = None - purpose: Optional[str] = None diff --git a/tpn/tpn/npm.py b/tpn/tpn/npm.py deleted file mode 100644 index cda6781822b3..000000000000 --- a/tpn/tpn/npm.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import asyncio -import io -import json -import pathlib -import tarfile - -import aiohttp - -from . import data - - -def _projects(package_data, overrides=frozenset()): - """Retrieve the list of projects from the package data. - - 'package_data' is assumed to be from a 'package-lock.json' file. All - dev-related dependencies are ignored. - - 'overrides' is assumed to be a set of npm package names which are known to - be included regardless of their "dev" dependency status. This commonly comes - up when use Webpack. - - """ - packages = {} - for name, details in package_data["dependencies"].items(): - if details.get("dev", False) and name not in overrides: - continue - packages[name] = data.Project(name, details["version"], url=details["resolved"]) - return packages - - -async def projects_from_data(raw_data, raw_overrides_data=None): - """Create projects from the file contents of a package-lock.json.""" - json_data = json.loads(raw_data) - if raw_overrides_data: - overrides_data = frozenset(json.loads(raw_overrides_data)) - else: - overrides_data = frozenset() - # "lockfileVersion": 1 - if "lockfileVersion" not in json_data: - raise ValueError("npm data does not appear to be from a package-lock.json file") - elif json_data["lockfileVersion"] != 1: - raise ValueError("unsupported package-lock.json format") - return _projects(json_data, overrides_data) - - -def _top_level_package_filenames(tarball_paths): - """Transform the iterable of npm tarball paths to the top-level files contained within the package.""" - paths = [] - for path in tarball_paths: - parts = pathlib.PurePath(path).parts - if parts[0] == "package" and len(parts) == 2: - paths.append(parts[1]) - return frozenset(paths) - - -# While ``name.lower().startswith("license")`` would works in all of the cases -# below, it is better to err on the side of being conservative and be explicit -# rather than just assume that there won't be an e.g. LICENCE_PLATES or LICENSEE -# file which isn't an actual license. -LICENSE_FILENAMES = frozenset( - x.lower() - for x in ( - "LICENCE", # Common typo. - "license", - "license.md", - "license.mkd", - "license.txt", - "LICENSE.BSD", - "LICENSE.MIT", - "LICENSE-MIT", - "LICENSE-MIT.txt", - ) -) - - -def _find_license(filenames): - """Find the file name for the license file.""" - for filename in filenames: - if filename.lower() in LICENSE_FILENAMES: - return filename - else: - raise ValueError(f"no license file found in {sorted(filenames)}") - - -async def _fetch_license(session, tarball_url): - """Download and extract the license file.""" - try: - async with session.get(tarball_url) as response: - response.raise_for_status() - content = await response.read() - with tarfile.open(mode="r:gz", fileobj=io.BytesIO(content)) as tarball: - filenames = _top_level_package_filenames(tarball.getnames()) - license_filename = _find_license(filenames) - with tarball.extractfile(f"package/{license_filename}") as file: - return file.read().decode("utf-8") - except Exception as exc: - return exc - - -async def fill_in_licenses(requested_projects): - """Add the missing licenses to requested_projects. - - Any failures in the searching for licenses are returned. - - """ - failures = {} - names = list(requested_projects.keys()) - urls = (requested_projects[name].url for name in names) - async with aiohttp.ClientSession() as session: - tasks = (_fetch_license(session, url) for url in urls) - for name, license_or_exc in zip(names, await asyncio.gather(*tasks)): - details = requested_projects[name] - license_or_exc = await _fetch_license(session, details.url) - if isinstance(license_or_exc, Exception): - details.error = license_or_exc - details.purpose = "npm" - failures[name] = details - else: - details.license = license_or_exc - print("⬡", end="", flush=True) - return failures diff --git a/tpn/tpn/tests/test_config.py b/tpn/tpn/tests/test_config.py deleted file mode 100644 index 19ea2d85c979..000000000000 --- a/tpn/tpn/tests/test_config.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import copy - -import pytest - -from .. import config - - -PROJECT_DATA = { - "Arch": { - "name": "Arch", - "version": "1.0.3", - "license": "Some license.\n\nHopefully it's a nice one.", - "url": "https://someplace.com/on/the/internet", - "purpose": "npm", - }, - "Python programming language": { - "name": "Python programming language", - "version": "3.6.5", - "license": "The PSF license.\n\nIt\nis\nvery\nlong!", - "url": "https://python.org", - "purpose": "explicit", - }, -} - - -@pytest.fixture -def example_data(): - return {name: config.ConfigProject(**data) for name, data in PROJECT_DATA.items()} - - -@pytest.fixture -def example_config(): - return {"metadata": {}, "project": [copy.deepcopy(details) for details in PROJECT_DATA.values()]} - - -def test_get_projects(example_config, example_data): - result = config.get_projects(example_config, {"npm", "explicit"}) - assert result == example_data - - -def test_get_projects_checks_sections(example_config): - example_config["PROJECCTS"] = [] - with pytest.raises(ValueError): - config.get_projects(example_config, {"npm"}) - - -def test_get_projects_key_check(example_config): - del example_config["project"][0]["url"] - with pytest.raises(KeyError): - config.get_projects(example_config, {"npm", "explicit"}) - - -def test_get_explicit_entries(example_data): - python_data = example_data["Python programming language"] - explicit_entries = config.get_explicit_entries(example_data) - assert explicit_entries == {"Python programming language": python_data} - assert "Python programming language" not in example_data - - -def test_sort_relevant(example_data): - expected = {"Arch": example_data["Arch"]} - npm_data = {"Arch": config.ConfigProject("Arch", "1.0.3", url="")} - relevant, stale = config.sort("npm", example_data, npm_data) - assert not stale - assert "Arch" not in npm_data - assert len(example_data) == 1 - assert relevant == expected - - -def test_sort_version_stale(example_data): - npm_data = {"Arch": config.ConfigProject("Arch", "2.0.0", url="")} - relevant, stale = config.sort("npm", example_data, npm_data) - assert not relevant - assert "Arch" in stale - assert stale["Arch"].version == "1.0.3" - assert "Arch" in npm_data - assert npm_data["Arch"].version == "2.0.0" - - -def test_sort_project_stale(example_data): - npm_data = {"Arch2": config.ConfigProject("Arch", "2.0.0", url="")} - relevant, stale = config.sort("npm", example_data, npm_data) - assert not relevant - assert "Arch" in stale - assert stale["Arch"].version == "1.0.3" - assert "Arch2" in npm_data - - -def test_sort_no_longer_relevant(example_data): - relevant, stale = config.sort("npm", example_data, {}) - assert not relevant - assert "Arch" in stale diff --git a/tpn/tpn/tests/test_npm.py b/tpn/tpn/tests/test_npm.py deleted file mode 100644 index 5a5676d0ad29..000000000000 --- a/tpn/tpn/tests/test_npm.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import json - -import pytest - -from .. import data -from .. import npm - - -@pytest.mark.asyncio -async def test_projects(): - json_data = { - "lockfileVersion": 1, - "dependencies": { - "append-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", - "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", - "dev": True, - "requires": {"buffer-equal": "^1.0.0"}, - }, - "applicationinsights": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.0.1.tgz", - "integrity": "sha1-U0Rrgw/o1dYZ7uKieLMdPSUDCSc=", - "requires": { - "diagnostic-channel": "0.2.0", - "diagnostic-channel-publishers": "0.2.1", - "zone.js": "0.7.6", - }, - }, - "arch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.0.tgz", - "integrity": "sha1-NhOqRhSQZLPB8GB5Gb8dR4boKIk=", - }, - "archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", - "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"}, - }, - }, - } - packages = await npm.projects_from_data(json.dumps(json_data)) - assert len(packages) == 2 - assert "arch" in packages - assert packages["arch"] == data.Project( - name="arch", - version="2.1.0", - url="https://registry.npmjs.org/arch/-/arch-2.1.0.tgz", - ) - assert "applicationinsights" in packages - assert packages["applicationinsights"] == data.Project( - name="applicationinsights", - version="1.0.1", - url="https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.0.1.tgz", - ) - - packages = await npm.projects_from_data(json.dumps(json_data), '["archy"]') - assert len(packages) == 3 - assert "arch" in packages - assert "applicationinsights" in packages - assert "archy" in packages - - modified_data = json_data.copy() - del modified_data["lockfileVersion"] - with pytest.raises(ValueError): - await npm.projects_from_data(json.dumps(modified_data)) - - modified_data = json_data.copy() - modified_data["lockfileVersion"] = 0 - with pytest.raises(ValueError): - await npm.projects_from_data(json.dumps(modified_data)) - - -def test_top_level_package_filenames(): - example = [ - "package/package.json", - "package/index.js", - "package/license", - "package/readme.md", - "package/code/stuff.js", - "i_do_not_know.txt", - ] - package_filenames = npm._top_level_package_filenames(example) - assert package_filenames == {"package.json", "index.js", "license", "readme.md"} - - -def test_find_license(): - example = {"package.json", "index.js", "license", "readme.md", "code/stuff.js"} - assert "license" == npm._find_license(example) - with pytest.raises(ValueError): - npm._find_license([]) - - -@pytest.mark.asyncio -async def test_fill_in_licenses(): - project = data.Project( - "user-home", - "2.0.0", - "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - ) - example = {"user-home": project} - failures = await npm.fill_in_licenses(example) - assert not failures - assert example["user-home"].license is not None diff --git a/tpn/tpn/tests/test_tpnfile.py b/tpn/tpn/tests/test_tpnfile.py deleted file mode 100644 index 48abd129c131..000000000000 --- a/tpn/tpn/tests/test_tpnfile.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import copy - -import pytest - -from .. import data -from .. import tpnfile - - -PROJECT_DATA = { - "Arch": { - "name": "Arch", - "version": "1.0.3", - "license": "Some license.\n\nHopefully it's a nice one.", - "url": "https://someplace.com/on/the/internet", - }, - "Python programming language": { - "name": "Python programming language", - "version": "3.6.5", - "license": "The PSF license.\n\nIt\nis\nvery\nlong!", - "url": "https://python.org", - }, -} - -EXAMPLE = """A header! - -With legal stuff! - - -1. Arch 1.0.3 (https://someplace.com/on/the/internet) -2. Python programming language 3.6.5 (https://python.org) - - -%% Arch 1.0.3 NOTICES AND INFORMATION BEGIN HERE (https://someplace.com/on/the/internet) -========================================= -Some license. - -Hopefully it's a nice one. -========================================= -END OF Arch NOTICES AND INFORMATION - -%% Python programming language 3.6.5 NOTICES AND INFORMATION BEGIN HERE (https://python.org) -========================================= -The PSF license. - -It -is -very -long! -========================================= -END OF Python programming language NOTICES AND INFORMATION -""" - - -@pytest.fixture -def example_data(): - return { - name: data.Project(**project_data) - for name, project_data in PROJECT_DATA.items() - } - - -def test_parse_tpn(example_data): - licenses = tpnfile.parse_tpn(EXAMPLE) - assert "Arch" in licenses - assert licenses["Arch"] == example_data["Arch"] - assert "Python programming language" in licenses - assert ( - licenses["Python programming language"] - == example_data["Python programming language"] - ) - - -def test_sort(example_data): - cached_data = copy.deepcopy(example_data) - requested_data = copy.deepcopy(example_data) - for details in requested_data.values(): - details.license = None - cached_data["Python programming language"].version = "1.5.2" - projects = tpnfile.sort(cached_data, requested_data) - assert not cached_data - assert len(requested_data) == 1 - assert "Python programming language" in requested_data - assert requested_data["Python programming language"].version == "3.6.5" - assert len(projects) == 1 - assert "Arch" in projects - assert projects["Arch"].license is not None - assert projects["Arch"].license == PROJECT_DATA["Arch"]["license"] - - -def test_generate_tpn(example_data): - settings = {"metadata": {"header": "A header!\n\nWith legal stuff!"}} - - assert tpnfile.generate_tpn(settings, example_data) == EXAMPLE diff --git a/tpn/tpn/tpnfile.py b/tpn/tpn/tpnfile.py deleted file mode 100644 index 689ffb3d456b..000000000000 --- a/tpn/tpn/tpnfile.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import dataclasses -import pathlib -import re - -from . import data - - -TPN_SECTION_TEMPLATE = "%% {name} {version} NOTICES AND INFORMATION BEGIN HERE ({url})\n=========================================\n{license}\n=========================================\nEND OF {name} NOTICES AND INFORMATION" -TPN_SECTION_RE = re.compile( - r"%% (?P.+?) (?P\S+) NOTICES AND INFORMATION BEGIN HERE \((?Phttp.+?)\)\n=========================================\n(?P.+?)\n=========================================\nEND OF .+? NOTICES AND INFORMATION", - re.DOTALL, -) - - -def parse_tpn(text): - """Break the TPN text up into individual project details.""" - licenses = {} - for match in TPN_SECTION_RE.finditer(text): - details = match.groupdict() - name = details["name"] - licenses[name] = data.Project(**details) - return licenses - - -def sort(cached_projects, requested_projects): - """Tease out the projects which have a valid cache entry. - - Both cached_projects and requested_projects are mutated as appropriate when - relevant cached entries are found. - - """ - projects = {} - for name, details in list(requested_projects.items()): - if name in cached_projects: - cached_details = cached_projects[name] - del cached_projects[name] - if cached_details.version == details.version: - projects[name] = cached_details - del requested_projects[name] - return projects - - -def generate_tpn(config, projects): - """Create the TPN text.""" - parts = [config["metadata"]["header"]] - project_names = sorted(projects.keys(), key=str.lower) - toc = [] - index_padding = len(f"{len(project_names)}.") - for index, name in enumerate(project_names, 1): - index_format = f"{index}.".ljust(index_padding) - toc.append( - f"{index_format} {name} {projects[name].version} ({projects[name].url})" - ) - parts.append("\n".join(toc)) - licenses = [] - for name in project_names: - details = projects[name] - licenses.append(TPN_SECTION_TEMPLATE.format(**dataclasses.asdict(details))) - parts.append("\n\n".join(licenses)) - return "\n\n\n".join(parts) + "\n" 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 bd9bd3a058d8..000000000000 --- a/tsconfig.datascience-ui.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es5", - "outDir": "out", - "lib": [ - "es6", - "dom" - ], - "jsx": "react", - "sourceMap": true, - "rootDir": "src", - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "noImplicitThis": false, - "noUnusedLocals": true, - "noUnusedParameters": false, - // "strict": true - }, - "exclude": [ - ".vscode-test", - "src/test", - "src/server", - "src/client", - "build" - ] -} diff --git a/tsconfig.extension.json b/tsconfig.extension.json index 22a940225adf..d5805806b675 100644 --- a/tsconfig.extension.json +++ b/tsconfig.extension.json @@ -5,9 +5,11 @@ "target": "es6", "outDir": "out", "lib": [ - "es6" + "es6", + "es2018", + "ES2019", + "ES2020", ], - "jsx": "react", "sourceMap": true, "rootDir": "src", "experimentalDecorators": true, @@ -17,7 +19,7 @@ "exclude": [ "node_modules", ".vscode-test", - "src/datascience-ui", + ".vscode test", "build" ] } diff --git a/tsconfig.json b/tsconfig.json index deb990fbc536..718d4ab4aad1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,29 +1,45 @@ { "compilerOptions": { - "module": "commonjs", - "target": "es6", + "baseUrl": ".", + "paths": { + "*": ["types/*"] + }, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "es2018", "outDir": "out", "lib": [ - "es6", "dom" + "es6", + "es2018", + "dom", + "ES2019", + "ES2020" ], - "jsx": "react", "sourceMap": true, "rootDir": "src", "experimentalDecorators": true, "allowSyntheticDefaultImports": true, - "noImplicitThis": false - // TODO: enable to ensure all code complies with strict coding standards - // , "noUnusedLocals": true - // , "noUnusedParameters": false - // , "strict": true + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "removeComments": true }, "exclude": [ "node_modules", ".vscode-test", + ".vscode test", "src/server/node_modules", "src/client/node_modules", "src/server/src/typings", "src/client/src/typings", - "build" + "src/smoke", + "build", + "out", + "tmp", + "pythonExtensionApi" ] } diff --git a/tsfmt.json b/tsfmt.json index fffcf07c1998..6d9806a01c23 100644 --- a/tsfmt.json +++ b/tsfmt.json @@ -1,17 +1,17 @@ { - "tabSize": 4, - "indentSize": 4, - "newLineCharacter": "\n", - "convertTabsToSpaces": false, - "insertSpaceAfterCommaDelimiter": true, - "insertSpaceAfterSemicolonInForStatements": true, - "insertSpaceBeforeAndAfterBinaryOperators": true, - "insertSpaceAfterKeywordsInControlFlowStatements": true, - "insertSpaceAfterFunctionKeywordForAnonymousFunctions": true, - "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, - "insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, - "insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, - "insertSpaceBeforeFunctionParenthesis": false, - "placeOpenBraceOnNewLineForFunctions": false, - "placeOpenBraceOnNewLineForControlBlocks": false + "tabSize": 4, + "indentSize": 4, + "newLineCharacter": "\n", + "convertTabsToSpaces": false, + "insertSpaceAfterCommaDelimiter": true, + "insertSpaceAfterSemicolonInForStatements": true, + "insertSpaceBeforeAndAfterBinaryOperators": true, + "insertSpaceAfterKeywordsInControlFlowStatements": true, + "insertSpaceAfterFunctionKeywordForAnonymousFunctions": true, + "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, + "insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, + "insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, + "insertSpaceBeforeFunctionParenthesis": false, + "placeOpenBraceOnNewLineForFunctions": false, + "placeOpenBraceOnNewLineForControlBlocks": false } diff --git a/tslint.json b/tslint.json deleted file mode 100644 index 58dd2d0a8a5f..000000000000 --- a/tslint.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "rulesDirectory": [ - "./build/tslint-rules" - ], - "extends": [ - "tslint-eslint-rules", - "tslint-microsoft-contrib" - ], - "rules": { - "messages-must-be-localized": true, - "copyright-and-strict-header": true, - "no-unused-expression": true, - "no-duplicate-variable": true, - "no-unused-variable": true, - "curly": true, - "class-name": true, - "semicolon": [ - true - ], - "triple-equals": true, - "no-relative-imports": false, - "max-line-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/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; + + /** 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; + } + + 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 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; + + /** + * 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; + + /** + * Event that fires when the {@link testResults} array is updated. + */ + export const onDidChangeTestResults: Event; + } + + export interface TestObserver { + /** + * List of tests returned by test provider for files in the workspace. + */ + readonly tests: ReadonlyArray; + + /** + * 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; + + /** + * 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; + + /** + * List of existing tests that have updated. + */ + readonly updated: ReadonlyArray; + + /** + * List of existing tests that have been removed. + */ + readonly removed: ReadonlyArray; + } + + /** + * 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>; + } + + /** + * 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; + + /** + * Optional list of nested tests for this item. + */ + readonly children: Readonly[]; + } + + 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; + } + + /** + * 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 { /** * Catches task errors and ignores them. */ - ignoreErrors(): void; + ignoreErrors(): Promise; } diff --git a/typings/index.d.ts b/typings/index.d.ts new file mode 100644 index 000000000000..7003ea5043b7 --- /dev/null +++ b/typings/index.d.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Added to allow compilation of backbone types pulled in from ipywidgets (@jupyterlab/widgets). +declare namespace JQuery { + type TriggeredEvent = unknown; +} diff --git a/typings/vscode-proposed/index.d.ts b/typings/vscode-proposed/index.d.ts new file mode 100644 index 000000000000..27d76adca192 --- /dev/null +++ b/typings/vscode-proposed/index.d.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable */ + +/* 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; + + /** + * 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; + } +} 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; + } +} 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; + } +} 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 {} diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 7955a6f511f6..000000000000 --- a/webpack.config.js +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -const merge = require('webpack-merge'); -const datascience = require('./webpack.datascience-ui.config.js'); -const extensionDependencies = require('./build/webpack/webpack.extension.dependencies.config.js').default; - -module.exports = [ - merge(datascience, { - devtool: 'eval' - }), - merge(extensionDependencies, { - mode: 'production', - devtool: 'source-map', - }) -]; diff --git a/webpack.datascience-ui.config.js b/webpack.datascience-ui.config.js deleted file mode 100644 index 5ef96cd8c2e3..000000000000 --- a/webpack.datascience-ui.config.js +++ /dev/null @@ -1,90 +0,0 @@ -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 configFileName = 'tsconfig.datascience-ui.json'; - -module.exports = { - entry: ['babel-polyfill', './src/datascience-ui/history-react/index.tsx'], - output: { - path: path.join(__dirname, 'out'), - filename: 'datascience-ui/history-react/index_bundle.js', - publicPath: './' - }, - - mode: 'development', // Leave as is, we'll need to see stack traces when there are errors. - // Use 'eval' for release and `eval-source-map` for development. - // We need to use one where source is embedded, due to webviews (they restrict resources to specific schemes, - // this seems to prevent chrome from downloading the source maps) - devtool: 'eval-source-map', - node: { - fs: 'empty' - }, - plugins: [ - new HtmlWebpackPlugin({ template: 'src/datascience-ui/history-react/index.html', imageBaseUrl: `${__dirname.replace(/\\/g, '/')}/out/datascience-ui/history-react`, indexUrl: `${__dirname}/out/1`, filename: './datascience-ui/history-react/index.html' }), - new FixDefaultImportPlugin(), - new CopyWebpackPlugin([ - { from: './**/*.png', to: '.' }, - { from: './**/*.svg', to: '.' }, - { from: './**/*.css', to: '.' }, - { from: './**/*theme*.json', to: '.' } - ], { context: 'src' }), - ], - resolve: { - // Add '.ts' and '.tsx' as resolvable extensions. - extensions: [".ts", ".tsx", ".js", ".json", ".svg"] - }, - - module: { - rules: [ - // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'. - { - test: /\.tsx?$/, - use: { - loader: "awesome-typescript-loader", - options: { - configFileName, - 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: {} - } - ] - } - ] - } -};